/******************************************************************************* * Copyright (c) 2015, 2016 EfficiOS Inc., Michael Jeanson * * All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.viewers; import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString; import java.math.BigDecimal; import java.text.Format; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.ToDoubleFunction; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import org.eclipse.tracecompass.common.core.format.DecimalUnitFormat; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiChartModel; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiResultTable; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTableEntry; import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiDecimalUnitFormat; import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiTimeStampFormat; import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.signals.LamiSelectionUpdateSignal; import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.views.LamiReportViewTabPage; import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler; import org.eclipse.tracecompass.tmf.ui.viewers.TmfViewer; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PlatformUI; import org.swtchart.Chart; import org.swtchart.ITitle; import com.google.common.collect.ImmutableList; /** * Abstract XYChart Viewer for LAMI views. * * @author Michael Jeanson * */ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer { /** Ellipsis character */ protected static final String ELLIPSIS = "…"; //$NON-NLS-1$ /** * String representing unknown values. Can be present even in numerical * aspects! */ protected static final String UNKNOWN = "?"; //$NON-NLS-1$ /** Zero long value */ protected static final long ZERO_LONG = 0L; /** Zero double value */ protected static final double ZERO_DOUBLE = 0.0; /** * Function to use to map Strings read from the data table to doubles for * use in SWTChart series. */ protected static final ToDoubleFunction<@Nullable String> DOUBLE_MAPPER = str -> { if (str == null || str.equals(UNKNOWN)) { return ZERO_LONG; } return Double.parseDouble(str); }; /** * List of standard colors */ protected static final List<@NonNull Color> COLORS = ImmutableList.of( new Color(Display.getDefault(), 72, 120, 207), new Color(Display.getDefault(), 106, 204, 101), new Color(Display.getDefault(), 214, 95, 95), new Color(Display.getDefault(), 180, 124, 199), new Color(Display.getDefault(), 196, 173, 102), new Color(Display.getDefault(), 119, 190, 219) ); /** * List of "light" colors (when unselected) */ protected static final List<@NonNull Color> LIGHT_COLORS = ImmutableList.of( new Color(Display.getDefault(), 173, 195, 233), new Color(Display.getDefault(), 199, 236, 197), new Color(Display.getDefault(), 240, 196, 196), new Color(Display.getDefault(), 231, 213, 237), new Color(Display.getDefault(), 231, 222, 194), new Color(Display.getDefault(), 220, 238, 246) ); /** * Time stamp formatter pattern for intervals in the days range. */ protected static final String DAYS_FORMATTER_PATTERN = "dd HH:mm"; //$NON-NLS-1$ /** * Time stamp formatter pattern for intervals in the hours range. */ protected static final String HOURS_FORMATTER_PATTERN = "HH:mm"; //$NON-NLS-1$ /** * Time stamp formatter pattern for intervals in the minutes range. */ protected static final String MINUTES_FORMATTER_PATTERN = "mm:ss"; //$NON-NLS-1$ /** * Time stamp formatter pattern for intervals in the seconds range. */ protected static final String SECONDS_FORMATTER_PATTERN = "ss"; //$NON-NLS-1$ /** * Time stamp formatter pattern for intervals in the milliseconds range. */ protected static final String MILLISECONDS_FORMATTER_PATTERN = "ss.SSS"; //$NON-NLS-1$ /** * Decimal formatter factor to display nanoseconds as seconds. */ protected static final double NANO_TO_SECS_FORMATTER_FACTOR = 0.000000001; /** * Default decimal formatter. */ protected static final DecimalUnitFormat DECIMAL_FORMATTER = new LamiDecimalUnitFormat(); /** Symbol for seconds (used in the custom ns -> s conversion) */ private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$ /** Symbol for nanoseconds (used in the custom ns -> s conversion) */ private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$ /** Maximum amount of digits that can be represented into a double */ private static final int BIG_DECIMAL_DIVISION_SCALE = 22; private final Listener fResizeListener = event -> { /* Refresh the titles to fit the current chart size */ refreshDisplayTitles(); /* Refresh the Axis labels to fit the current chart size */ refreshDisplayLabels(); }; private final LamiChartModel fChartModel; private final LamiReportViewTabPage fPage; private final Chart fChart; private final String fChartTitle; private String fXLabel; private @Nullable String fXUnits; private String fYLabel; private @Nullable String fYUnits; private boolean fSelected; private Set<Integer> fSelection; private final ToolBar fToolBar; /** * Creates a Viewer instance based on SWTChart. * * @param parent * The parent composite to draw in. * @param page * The {@link LamiReportViewTabPage} parent page * @param chartModel * The information about the chart to build */ public LamiXYChartViewer(Composite parent, LamiReportViewTabPage page, LamiChartModel chartModel) { super(parent); fParent = parent; fPage = page; fChartModel = chartModel; fSelection = new HashSet<>(); fXLabel = ""; //$NON-NLS-1$ fYLabel = ""; //$NON-NLS-1$ fChart = new Chart(parent, SWT.NONE); fChart.addListener(SWT.Resize, fResizeListener); /* Set Chart title */ fChartTitle = fPage.getResultTable().getTableClass().getTableTitle(); /* Set X axis title */ if (fChartModel.getXSeriesColumns().size() == 1) { /* * There is only 1 series in the chart, we will use its name as the * X axis. */ innerSetXTitle(getXAxisAspects().get(0).getName(), getXAxisAspects().get(0).getUnits()); } else { /* * There are multiple series in the chart, if they all share the same * units, display that. */ long nbDiffAspectsUnits = getXAxisAspects().stream() .map(aspect -> aspect.getUnits()) .distinct() .count(); long nbDiffAspectName = getXAxisAspects().stream() .map(aspect -> aspect.getName()) .distinct() .count(); String xBaseTitle = Messages.LamiViewer_DefaultValueName; if (nbDiffAspectName == 1) { xBaseTitle = getXAxisAspects().get(0).getName(); } String units = null; if (nbDiffAspectsUnits == 1) { /* All aspects use the same unit type */ units = getXAxisAspects().get(0).getUnits(); } innerSetXTitle(xBaseTitle, units); } /* Set Y axis title */ if (fChartModel.getYSeriesColumns().size() == 1) { /* * There is only 1 series in the chart, we will use its name as the * Y axis (and hide the legend). */ innerSetYTitle(getYAxisAspects().get(0).getName(), getYAxisAspects().get(0).getUnits()); /* Hide the legend */ fChart.getLegend().setVisible(false); } else { /* * There are multiple series in the chart, if they all share the same * units, display that. */ long nbDiffAspectsUnits = getYAxisAspects().stream() .map(aspect -> aspect.getUnits()) .distinct() .count(); long nbDiffAspectName = getYAxisAspects().stream() .map(aspect -> aspect.getName()) .distinct() .count(); String yBaseTitle = Messages.LamiViewer_DefaultValueName; if (nbDiffAspectName == 1) { yBaseTitle = getYAxisAspects().get(0).getName(); } String units = null; if (nbDiffAspectsUnits == 1) { /* All aspects use the same unit type */ units = getYAxisAspects().get(0).getUnits(); } innerSetYTitle(yBaseTitle, units); /* Put legend at the bottom */ fChart.getLegend().setPosition(SWT.BOTTOM); } /* Set all titles and labels font color to black */ fChart.getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); fChart.getAxisSet().getXAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); fChart.getAxisSet().getYAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); fChart.getAxisSet().getXAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); fChart.getAxisSet().getYAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); /* Set X label 90 degrees */ fChart.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90); /* Refresh the titles to fit the current chart size */ refreshDisplayTitles(); fToolBar = createChartToolBar(); fChart.addDisposeListener(e -> { /* Dispose resources of this class */ LamiXYChartViewer.super.dispose(); }); } /** * Set the Y axis title and refresh the chart. * * @param label * the label string. * @param units * the units string. */ protected void setYTitle(@Nullable String label, @Nullable String units) { innerSetYTitle(label, units); } private void innerSetYTitle(@Nullable String label, @Nullable String units) { fYLabel = nullToEmptyString(label); innerSetYUnits(units); refreshDisplayTitles(); } /** * Set the units on the Y Axis title and refresh the chart. * * @param units * the units string. */ protected void setYUnits(@Nullable String units) { innerSetYUnits(units); } private void innerSetYUnits(@Nullable String units) { /* * All time durations in the Lami protocol are nanoseconds, on the * charts we use an axis formater that converts back to seconds as a * base unit and then uses prefixes like nano and milli depending on the * range. * * So set the units to seconds in the title to match the base unit of * the formater. */ if (NANOSECONDS_SYMBOL.equals(units)) { fYUnits = SECONDS_SYMBOL; } else { fYUnits = units; } refreshDisplayTitles(); } /** * Get the Y axis title string. * * If the units is non-null, the title will be: "label (units)" * * If the units is null, the title will be: "label" * * @return the title of the Y axis. */ protected String getYTitle() { if (fYUnits == null) { return fYLabel; } return fYLabel + " (" + fYUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } /** * Set the X axis title and refresh the chart. * * @param label * the label string. * @param units * the units string. */ protected void setXTitle(@Nullable String label, @Nullable String units) { innerSetXTitle(label, units); } private void innerSetXTitle(@Nullable String label, @Nullable String units) { fXLabel = nullToEmptyString(label); innerSetXUnits(units); refreshDisplayTitles(); } /** * Set the units on the X Axis title. * * @param units * the units string */ protected void setXUnits(@Nullable String units) { innerSetXUnits(units); } private void innerSetXUnits(@Nullable String units) { /* The time duration formatter converts ns to s on the axis */ if (NANOSECONDS_SYMBOL.equals(units)) { fXUnits = SECONDS_SYMBOL; } else { fXUnits = units; } refreshDisplayTitles(); } /** * Get the X axis title string. * * If the units is non-null, the title will be: "label (units)" * * If the units is null, the title will be: "label" * * @return the title of the Y axis. */ protected String getXTitle() { if (fXUnits == null) { return fXLabel; } return fXLabel + " (" + fXUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } /** * Util method to check if a list of aspects are all continuous. * * @param axisAspects * The list of aspects to check. * @return true is all aspects are continuous, otherwise false. */ protected static boolean areAspectsContinuous(List<LamiTableEntryAspect> axisAspects) { return axisAspects.stream().allMatch(aspect -> aspect.isContinuous()); } /** * Util method to check if a list of aspects are all time stamps. * * @param axisAspects * The list of aspects to check. * @return true is all aspects are time stamps, otherwise false. */ protected static boolean areAspectsTimeStamp(List<LamiTableEntryAspect> axisAspects) { return axisAspects.stream().allMatch(aspect -> aspect.isTimeStamp()); } /** * Util method to check if a list of aspects are all time durations. * * @param axisAspects * The list of aspects to check. * @return true is all aspects are time durations, otherwise false. */ protected static boolean areAspectsTimeDuration(List<LamiTableEntryAspect> axisAspects) { return axisAspects.stream().allMatch(aspect -> aspect.isTimeDuration()); } /** * Util method that will return a formatter based on the aspects linked to an axis * * If all aspects are time stamps, return a timestamp formatter tuned to the interval. * If all aspects are time durations, return the nanoseconds to seconds formatter. * Otherwise, return the generic decimal formatter. * * @param axisAspects * The list of aspects of the axis. * @param entries * The list of entries of the chart. * @param internalRange * The internal range for value transformation * @param externalRange * The external range for value transformation * @return a formatter for the axis. */ protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries , @Nullable LamiGraphRange internalRange, @Nullable LamiGraphRange externalRange) { Format formatter = null; if (areAspectsTimeStamp(axisAspects)) { /* Set a TimeStamp formatter depending on the duration between the first and last value */ BigDecimal max = new BigDecimal(Long.MIN_VALUE); BigDecimal min = new BigDecimal(Long.MAX_VALUE); for (LamiTableEntry entry : entries) { for (LamiTableEntryAspect aspect : axisAspects) { @Nullable Number number = aspect.resolveNumber(entry); if (number != null) { BigDecimal current = new BigDecimal(number.toString()); max = current.max(max); min = current.min(min); } } } long duration = max.subtract(min).longValue(); if (duration > TimeUnit.DAYS.toNanos(1)) { formatter = new LamiTimeStampFormat(DAYS_FORMATTER_PATTERN, internalRange, externalRange); } else if (duration > TimeUnit.HOURS.toNanos(1)) { formatter = new LamiTimeStampFormat(HOURS_FORMATTER_PATTERN, internalRange, externalRange); } else if (duration > TimeUnit.MINUTES.toNanos(1)) { formatter = new LamiTimeStampFormat(MINUTES_FORMATTER_PATTERN, internalRange, externalRange); } else if (duration > TimeUnit.SECONDS.toNanos(15)) { formatter = new LamiTimeStampFormat(SECONDS_FORMATTER_PATTERN, internalRange, externalRange); } else { formatter = new LamiTimeStampFormat(MILLISECONDS_FORMATTER_PATTERN, internalRange, externalRange); } } else if (areAspectsTimeDuration(axisAspects)) { /* Set the time duration formatter. */ formatter = new LamiDecimalUnitFormat(NANO_TO_SECS_FORMATTER_FACTOR, internalRange, externalRange); } else { /* * For other numeric aspects, use the default lami decimal unit * formatter. */ formatter = new LamiDecimalUnitFormat(internalRange, externalRange); } return formatter; } /** * Get the chart result table. * * @return The chart result table. */ protected LamiResultTable getResultTable() { return fPage.getResultTable(); } /** * Get the chart {@code LamiReportViewTabPage} parent. * * @return The {@code LamiReportViewTabPage} parent. */ protected LamiReportViewTabPage getPage() { return fPage; } /** * Get the chart model. * * @return The chart model. */ protected LamiChartModel getChartModel() { return fChartModel; } /** * Get the chart object. * @return The chart object. */ protected Chart getChart() { return fChart; } /** * @return the toolBar */ public ToolBar getToolBar() { return fToolBar; } /** * Is a selection made in the chart. * * @return true if there is a selection. */ protected boolean isSelected() { return fSelected; } /** * Set the selection index. * * @param selection the index to select. */ protected void setSelection(Set<Integer> selection) { fSelection = selection; fSelected = !selection.isEmpty(); } /** * Unset the chart selection. */ protected void unsetSelection() { fSelection.clear(); fSelected = false; } /** * Get the current selection index. * * @return the current selection index. */ protected Set<Integer> getSelection() { return fSelection; } @Override public @Nullable Control getControl() { return fChart.getParent(); } @Override public void refresh() { Display.getDefault().asyncExec(() -> { if (!fChart.isDisposed()) { fChart.redraw(); } }); } @Override public void dispose() { fChart.dispose(); /* The control's DisposeListener will call super.dispose() */ } /** * Get a list of all the aspect of the Y axis. * * @return The aspects for the Y axis */ protected List<LamiTableEntryAspect> getYAxisAspects() { List<LamiTableEntryAspect> yAxisAspects = new ArrayList<>(); for (String colName : getChartModel().getYSeriesColumns()) { yAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); } return yAxisAspects; } /** * Get a list of all the aspect of the X axis. * * @return The aspects for the X axis */ protected List<LamiTableEntryAspect> getXAxisAspects() { List<LamiTableEntryAspect> xAxisAspects = new ArrayList<>(); for (String colName : getChartModel().getXSeriesColumns()) { xAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); } return xAxisAspects; } /** * Set the ITitle object text to a substring of canonicalTitle that when * rendered in the chart will fit maxPixelLength. */ private void refreshDisplayTitle(ITitle title, String canonicalTitle, int maxPixelLength) { if (title.isVisible()) { String newTitle = canonicalTitle; /* Get the title font */ Font font = title.getFont(); GC gc = new GC(fParent); gc.setFont(font); /* Get the length and height of the canonical title in pixels */ Point pixels = gc.stringExtent(canonicalTitle); /* * If the title is too long, generate a shortened version based on the * average character width of the current font. */ if (pixels.x > maxPixelLength) { int charwidth = gc.getFontMetrics().getAverageCharWidth(); int minimum = 3; int strLen = ((maxPixelLength / charwidth) - minimum); if (strLen > minimum) { newTitle = canonicalTitle.substring(0, strLen) + ELLIPSIS; } else { newTitle = ELLIPSIS; } } title.setText(newTitle); // Cleanup gc.dispose(); } } /** * Refresh the Chart, XAxis and YAxis titles to fit the current * chart size. */ private void refreshDisplayTitles() { Rectangle chartRect = fChart.getClientArea(); Rectangle plotRect = fChart.getPlotArea().getClientArea(); ITitle chartTitle = checkNotNull(fChart.getTitle()); refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width); ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle()); refreshDisplayTitle(xTitle, getXTitle(), plotRect.width); ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle()); refreshDisplayTitle(yTitle, getYTitle(), plotRect.height); } /** * Get the aspect with the given name * * @param aspects * The list of aspects to search into * @param aspectName * The name of the aspect we are looking for * @return The corresponding aspect */ protected static @Nullable LamiTableEntryAspect getAspectFromName(List<LamiTableEntryAspect> aspects, String aspectName) { for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { if (lamiTableEntryAspect.getLabel().equals(aspectName)) { return lamiTableEntryAspect; } } return null; } /** * Refresh the axis labels to fit the current chart size. */ protected abstract void refreshDisplayLabels(); /** * Redraw the chart. */ protected void redraw() { refresh(); } /** * Signal handler for selection update. * * @param signal * The selection update signal */ @TmfSignalHandler public void updateSelection(LamiSelectionUpdateSignal signal) { if (getPage() != signal.getSignalKey() || equals(signal.getSource())) { /* The signal is not for us */ return; } setSelection(signal.getEntryIndex()); redraw(); } /** * Create a tool bar on top right of the chart. Contained actions: * <ul> * <li>Dispose the current viewer, also known as "Close the chart"</li> * </ul> * * This tool bar should only appear when the mouse enters the composite. * * @return the tool bar */ protected ToolBar createChartToolBar() { Image removeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE); ToolBar toolBar = new ToolBar(getChart(), SWT.HORIZONTAL); /* Default state */ toolBar.moveAbove(null); toolBar.setVisible(false); /* * Close chart button */ ToolItem closeButton = new ToolItem(toolBar, SWT.PUSH); closeButton.setImage(removeImage); closeButton.setToolTipText(Messages.LamiXYChartViewer_CloseChartToolTip); closeButton.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(@Nullable SelectionEvent e) { Composite parent = getParent(); dispose(); parent.layout(); } @Override public void widgetDefaultSelected(@Nullable SelectionEvent e) { } }); toolBar.pack(); toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); /* Visibility toggle filter */ Listener toolBarVisibilityToggleListener = e -> { if (e.widget instanceof Control) { Control control = (Control) e.widget; Point display = control.toDisplay(e.x, e.y); Point location = getChart().getParent().toControl(display); /* * Only set to visible if we are at the right location, in the * right shell. */ boolean visible = getChart().getBounds().contains(location) && control.getShell().equals(getChart().getShell()); getToolBar().setVisible(visible); } }; /* Filter to make sure we hide the toolbar if we exit the window */ Listener hideToolBarListener = (e -> getToolBar().setVisible(false)); /* * Add the filters to the main Display, and remove them when we dispose * the chart. */ Display display = getChart().getDisplay(); display.addFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); display.addFilter(SWT.MouseExit, hideToolBarListener); getChart().addDisposeListener(e -> { display.removeFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); display.removeFilter(SWT.MouseExit, hideToolBarListener); }); /* Reposition the tool bar on resize */ getChart().addListener(SWT.Resize, new Listener() { @Override public void handleEvent(@Nullable Event event) { toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); } }); return toolBar; } /** * Get a {@link LamiGraphRange} that covers all data points in the result * table. * <p> * The returned range will be the minimum and maximum of the resolved values * of the passed aspects for all result entries. If <code>clampToZero</code> * is true, a positive minimum value will be clamped down to zero. * * @param aspects * The aspects that the range will represent * @param clampToZero * If true, a positive minimum value will be clamped down to zero * @return the range */ protected LamiGraphRange getRange(List<LamiTableEntryAspect> aspects, boolean clampToZero) { /* Find the minimum and maximum values */ BigDecimal min = new BigDecimal(Long.MAX_VALUE); BigDecimal max = new BigDecimal(Long.MIN_VALUE); for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { for (LamiTableEntry entry : getResultTable().getEntries()) { @Nullable Number number = lamiTableEntryAspect.resolveNumber(entry); if (number != null) { BigDecimal current = new BigDecimal(number.toString()); min = current.min(min); max = current.max(max); } } } if (clampToZero) { min = min.min(BigDecimal.ZERO); } /* Do not allow a range with a zero delta default to 1 */ if (max.equals(min)) { max = min.add(BigDecimal.ONE); } return new LamiGraphRange(checkNotNull(min), checkNotNull(max)); } /** * Transform an external value into an internal value. Since SWTChart only * support Double and Lami can pass Long values, loss of precision might * happen. To minimize this, transform the raw values to an internal * representation based on a linear transformation. * * The internal value = * * ((rawValue - rawMinimum) * (internalRangeDelta/rawRangeDelta)) + * internalMinimum * * @param number * The number to transform * @param internalRange * The internal range definition to be used * @param externalRange * The external range definition to be used * @return the transformed value in Double comprised inside the internal * range */ protected static double getInternalDoubleValue(Number number, LamiGraphRange internalRange, LamiGraphRange externalRange) { BigDecimal value = new BigDecimal(number.toString()); if (externalRange.getDelta().compareTo(BigDecimal.ZERO) == 0) { return internalRange.getMinimum().doubleValue(); } BigDecimal internalValue = value .subtract(externalRange.getMinimum()) .multiply(internalRange.getDelta()) .divide(externalRange.getDelta(), BIG_DECIMAL_DIVISION_SCALE, BigDecimal.ROUND_DOWN) .add(internalRange.getMinimum()); return internalValue.doubleValue(); } }