package name.abuchen.portfolio.ui.views; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.ToolBar; import org.swtchart.ISeries; import name.abuchen.portfolio.snapshot.Aggregation; import name.abuchen.portfolio.snapshot.PerformanceIndex; import name.abuchen.portfolio.ui.Images; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.PortfolioPart; import name.abuchen.portfolio.ui.PortfolioPlugin; import name.abuchen.portfolio.ui.util.AbstractCSVExporter; import name.abuchen.portfolio.ui.util.AbstractDropDown; import name.abuchen.portfolio.ui.util.SimpleAction; import name.abuchen.portfolio.ui.util.chart.TimelineChart; import name.abuchen.portfolio.ui.util.chart.TimelineChartCSVExporter; import name.abuchen.portfolio.ui.views.dataseries.DataSeries; import name.abuchen.portfolio.ui.views.dataseries.DataSeriesCache; import name.abuchen.portfolio.ui.views.dataseries.DataSeriesChartLegend; import name.abuchen.portfolio.ui.views.dataseries.DataSeriesConfigurator; import name.abuchen.portfolio.ui.views.dataseries.PerformanceChartSeriesBuilder; public class PerformanceChartView extends AbstractHistoricView { private static final String KEY_AGGREGATION_PERIOD = "performance-chart-aggregation-period"; //$NON-NLS-1$ private TimelineChart chart; private DataSeriesConfigurator picker; private Aggregation.Period aggregationPeriod; private PerformanceChartSeriesBuilder seriesBuilder; @Override protected String getDefaultTitle() { return Messages.LabelPerformanceChart; } @Override public void init(PortfolioPart part, Object parameter) { super.init(part, parameter); String key = part.getPreferenceStore().getString(KEY_AGGREGATION_PERIOD); if (key != null && key.length() > 0) { try { aggregationPeriod = Aggregation.Period.valueOf(key); } catch (IllegalArgumentException ignore) { // ignore if key is not a valid aggregation period PortfolioPlugin.log(ignore); } } } @Override protected void addButtons(ToolBar toolBar) { super.addButtons(toolBar); new AggregationPeriodDropDown(toolBar); new ExportDropDown(toolBar); addConfigButton(toolBar); } private void addConfigButton(ToolBar toolBar) { Action save = new Action() { @Override public void run() { picker.showSaveMenu(getActiveShell()); } }; save.setImageDescriptor(Images.SAVE.descriptor()); save.setToolTipText(Messages.MenuSaveChart); new ActionContributionItem(save).fill(toolBar, -1); Action config = new Action() { @Override public void run() { picker.showMenu(getActiveShell()); } }; config.setImageDescriptor(Images.CONFIG.descriptor()); config.setToolTipText(Messages.MenuConfigureChart); new ActionContributionItem(config).fill(toolBar, -1); } @Override protected Composite createBody(Composite parent) { Composite composite = new Composite(parent, SWT.NONE); composite.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); chart = new TimelineChart(composite); chart.getTitle().setText(getTitle()); chart.getTitle().setVisible(false); chart.getAxisSet().getYAxis(0).getTick().setFormat(new DecimalFormat("0.#%")); //$NON-NLS-1$ chart.getToolTip().setValueFormat(new DecimalFormat("0.##%")); //$NON-NLS-1$ DataSeriesCache cache = make(DataSeriesCache.class); seriesBuilder = new PerformanceChartSeriesBuilder(chart, cache); picker = new DataSeriesConfigurator(this, DataSeries.UseCase.PERFORMANCE); picker.addListener(() -> updateChart()); DataSeriesChartLegend legend = new DataSeriesChartLegend(composite, picker); updateTitle(Messages.LabelPerformanceChart + " (" + picker.getConfigurationName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ chart.getTitle().setText(getTitle()); GridLayoutFactory.fillDefaults().numColumns(1).margins(0, 0).spacing(0, 0).applyTo(composite); GridDataFactory.fillDefaults().grab(true, true).applyTo(chart); GridDataFactory.fillDefaults().grab(true, false).align(SWT.CENTER, SWT.FILL).applyTo(legend); setChartSeries(); return composite; } @Override public void setFocus() { chart.adjustRange(); chart.setFocus(); } @Override public void reportingPeriodUpdated() { seriesBuilder.getCache().clear(); updateChart(); } @Override public void notifyModelUpdated() { seriesBuilder.getCache().clear(); updateChart(); } private void updateChart() { try { updateTitle(Messages.LabelPerformanceChart + " (" + picker.getConfigurationName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ chart.suspendUpdate(true); chart.getTitle().setText(getTitle()); for (ISeries s : chart.getSeriesSet().getSeries()) chart.getSeriesSet().deleteSeries(s.getId()); setChartSeries(); chart.adjustRange(); } finally { chart.suspendUpdate(false); } chart.redraw(); // re-layout in case chart legend changed chart.getParent().layout(true); } private void setChartSeries() { picker.getSelectedDataSeries() .forEach(series -> seriesBuilder.build(series, getReportingPeriod(), aggregationPeriod)); } private final class AggregationPeriodDropDown extends AbstractDropDown { private AggregationPeriodDropDown(ToolBar toolBar) { super(toolBar, aggregationPeriod == null ? Messages.LabelAggregationDaily : aggregationPeriod.toString()); } @Override public void menuAboutToShow(IMenuManager manager) { Action daily = new Action(Messages.LabelAggregationDaily) { @Override public void run() { setLabel(Messages.LabelAggregationDaily); aggregationPeriod = null; getPart().getPreferenceStore().setValue(KEY_AGGREGATION_PERIOD, ""); //$NON-NLS-1$ updateChart(); } }; daily.setChecked(aggregationPeriod == null); manager.add(daily); for (final Aggregation.Period period : Aggregation.Period.values()) { Action action = new Action(period.toString()) { @Override public void run() { setLabel(period.toString()); aggregationPeriod = period; getPart().getPreferenceStore().setValue(KEY_AGGREGATION_PERIOD, period.name()); updateChart(); } }; action.setChecked(aggregationPeriod == period); manager.add(action); } } } private final class ExportDropDown extends AbstractDropDown { private ExportDropDown(ToolBar toolBar) { super(toolBar, Messages.MenuExportData, Images.EXPORT.image(), SWT.NONE); } @Override public void menuAboutToShow(IMenuManager manager) { manager.add(new Action(Messages.MenuExportChartData) { @Override public void run() { TimelineChartCSVExporter exporter = new TimelineChartCSVExporter(chart); exporter.addDiscontinousSeries(Messages.PerformanceChartLabelCPI); exporter.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); //$NON-NLS-1$ exporter.setValueFormat(new DecimalFormat("0.##########")); //$NON-NLS-1$ exporter.export(getTitle() + ".csv"); //$NON-NLS-1$ } }); picker.getSelectedDataSeries().stream() // .filter(ds -> ds.getType() != DataSeries.Type.CONSUMER_PRICE_INDEX) .forEach(ds -> addMenu(manager, ds)); manager.add(new Separator()); chart.exportMenuAboutToShow(manager, getTitle()); } private void addMenu(IMenuManager manager, final DataSeries series) { manager.add(new SimpleAction(MessageFormat.format(Messages.LabelExport, series.getLabel()), a -> { AbstractCSVExporter exporter = new AbstractCSVExporter() { @Override protected void writeToFile(File file) throws IOException { PerformanceIndex index = seriesBuilder.getCache().lookup(series, getReportingPeriod()); if (aggregationPeriod != null) index = Aggregation.aggregate(index, aggregationPeriod); index.exportTo(file); } @Override protected Control getControl() { return ExportDropDown.this.getToolBar(); } }; exporter.export(getTitle() + "_" + series.getLabel() + ".csv"); //$NON-NLS-1$ //$NON-NLS-2$ })); } } }