package name.abuchen.portfolio.ui.views.dashboard; import java.text.DecimalFormat; import java.util.Arrays; import java.util.List; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.swtchart.ISeries; import name.abuchen.portfolio.model.ConfigurationSet; import name.abuchen.portfolio.model.Dashboard; import name.abuchen.portfolio.model.Dashboard.Widget; import name.abuchen.portfolio.snapshot.Aggregation; import name.abuchen.portfolio.snapshot.ReportingPeriod; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.PortfolioPlugin; import name.abuchen.portfolio.ui.util.LabelOnly; import name.abuchen.portfolio.ui.util.SimpleAction; import name.abuchen.portfolio.ui.util.chart.TimelineChart; import name.abuchen.portfolio.ui.views.PerformanceChartView; import name.abuchen.portfolio.ui.views.StatementOfAssetsHistoryView; import name.abuchen.portfolio.ui.views.dataseries.DataSeries; import name.abuchen.portfolio.ui.views.dataseries.DataSeriesConfigurator; import name.abuchen.portfolio.ui.views.dataseries.DataSeriesSerializer; import name.abuchen.portfolio.ui.views.dataseries.DataSeriesSet; import name.abuchen.portfolio.ui.views.dataseries.PerformanceChartSeriesBuilder; import name.abuchen.portfolio.ui.views.dataseries.StatementOfAssetsSeriesBuilder; public class ChartWidget extends WidgetDelegate { private class ChartConfig implements WidgetConfig { private WidgetDelegate delegate; private ConfigurationSet configSet; private ConfigurationSet.Configuration config; public ChartConfig(WidgetDelegate delegate, DataSeries.UseCase useCase) { this.delegate = delegate; String configName = (useCase == DataSeries.UseCase.STATEMENT_OF_ASSETS ? StatementOfAssetsHistoryView.class : PerformanceChartView.class).getSimpleName() + DataSeriesConfigurator.IDENTIFIER_POSTFIX; configSet = delegate.getClient().getSettings().getConfigurationSet(configName); String uuid = delegate.getWidget().getConfiguration().get(Dashboard.Config.CONFIG_UUID.name()); config = configSet.lookup(uuid).orElseGet(() -> configSet.getConfigurations().findFirst() .orElseGet(() -> new ConfigurationSet.Configuration(Messages.LabelNoName, null))); } @Override public void menuAboutToShow(IMenuManager manager) { manager.appendToGroup(DashboardView.INFO_MENU_GROUP_NAME, new LabelOnly(config != null ? config.getName() : Messages.LabelNoName)); MenuManager subMenu = new MenuManager(Messages.ClientEditorLabelChart); this.configSet.getConfigurations().forEach(c -> { SimpleAction action = new SimpleAction(c.getName(), a -> { config = c; delegate.getWidget().getConfiguration().put(Dashboard.Config.CONFIG_UUID.name(), c.getUUID()); delegate.getClient().markDirty(); }); action.setChecked(c.equals(config)); subMenu.add(action); }); manager.add(subMenu); } public String getData() { return config != null ? config.getData() : null; } @Override public String getLabel() { return Messages.ClientEditorLabelChart + ": " //$NON-NLS-1$ + (config != null ? config.getName() : Messages.LabelNoName); } } private class AggregationConfig implements WidgetConfig { private WidgetDelegate delegate; private Aggregation.Period aggregation; public AggregationConfig(WidgetDelegate delegate) { this.delegate = delegate; try { String code = delegate.getWidget().getConfiguration().get(Dashboard.Config.AGGREGATION.name()); if (code != null) this.aggregation = Aggregation.Period.valueOf(code); } catch (IllegalArgumentException ignore) { PortfolioPlugin.log(ignore); } } @Override public void menuAboutToShow(IMenuManager manager) { manager.appendToGroup(DashboardView.INFO_MENU_GROUP_NAME, new LabelOnly( aggregation != null ? aggregation.toString() : Messages.LabelAggregationDaily)); MenuManager subMenu = new MenuManager(Messages.LabelAggregation); Action action = new SimpleAction(Messages.LabelAggregationDaily, a -> { aggregation = null; delegate.getWidget().getConfiguration().remove(Dashboard.Config.AGGREGATION.name()); delegate.getClient().markDirty(); }); action.setChecked(aggregation == null); subMenu.add(action); Arrays.stream(Aggregation.Period.values()).forEach(a -> { Action menu = new SimpleAction(a.toString(), x -> { aggregation = a; delegate.getWidget().getConfiguration().put(Dashboard.Config.AGGREGATION.name(), a.name()); delegate.getClient().markDirty(); }); menu.setChecked(aggregation == a); subMenu.add(menu); }); manager.add(subMenu); } public Aggregation.Period getAggregation() { return aggregation; } @Override public String getLabel() { return Messages.LabelAggregation + ": " + //$NON-NLS-1$ (aggregation != null ? aggregation.toString() : Messages.LabelAggregationDaily); } } private DataSeries.UseCase useCase; private DataSeriesSet dataSeriesSet; private Label title; private TimelineChart chart; public ChartWidget(Widget widget, DashboardData dashboardData, DataSeries.UseCase useCase) { super(widget, dashboardData); this.useCase = useCase; this.dataSeriesSet = new DataSeriesSet(dashboardData.getClient(), dashboardData.getPreferences(), useCase); addConfig(new ChartConfig(this, useCase)); if (useCase == DataSeries.UseCase.PERFORMANCE) addConfig(new AggregationConfig(this)); addConfig(new ReportingPeriodConfig(this)); } @Override public Composite createControl(Composite parent, DashboardResources resources) { Composite container = new Composite(parent, SWT.NONE); GridLayoutFactory.fillDefaults().numColumns(1).margins(5, 5).applyTo(container); container.setBackground(parent.getBackground()); title = new Label(container, SWT.NONE); title.setText(getWidget().getLabel()); GridDataFactory.fillDefaults().grab(true, false).applyTo(title); chart = new TimelineChart(container); chart.getTitle().setVisible(false); chart.getTitle().setText(title.getText()); chart.getAxisSet().getYAxis(0).getTick().setVisible(false); if (useCase != DataSeries.UseCase.STATEMENT_OF_ASSETS) chart.getToolTip().setValueFormat(new DecimalFormat("0.##%")); //$NON-NLS-1$ GC gc = new GC(container); gc.setFont(resources.getKpiFont()); Point stringExtend = gc.stringExtent("X"); //$NON-NLS-1$ gc.dispose(); GridDataFactory.fillDefaults().hint(SWT.DEFAULT, stringExtend.y * 6).grab(true, false).applyTo(chart); container.layout(); return container; } @Override Control getTitleControl() { return title; } @Override public void update() { title.setText(getWidget().getLabel()); try { chart.suspendUpdate(true); chart.getTitle().setText(title.getText()); for (ISeries s : chart.getSeriesSet().getSeries()) chart.getSeriesSet().deleteSeries(s.getId()); List<DataSeries> series = new DataSeriesSerializer().fromString(dataSeriesSet, get(ChartConfig.class).getData()); ReportingPeriod reportingPeriod = get(ReportingPeriodConfig.class).getReportingPeriod(); switch (useCase) { case STATEMENT_OF_ASSETS: buildAssetSeries(series, reportingPeriod); break; case PERFORMANCE: buildPerformanceSeries(series, reportingPeriod); break; case RETURN_VOLATILITY: throw new UnsupportedOperationException(); default: throw new IllegalArgumentException(); } chart.adjustRange(); } finally { chart.suspendUpdate(false); } chart.redraw(); } private void buildAssetSeries(List<DataSeries> series, ReportingPeriod reportingPeriod) { StatementOfAssetsSeriesBuilder b1 = new StatementOfAssetsSeriesBuilder(chart, getDashboardData().getDataSeriesCache()); series.forEach(s -> b1.build(s, reportingPeriod)); } private void buildPerformanceSeries(List<DataSeries> series, ReportingPeriod reportingPeriod) { PerformanceChartSeriesBuilder b2 = new PerformanceChartSeriesBuilder(chart, getDashboardData().getDataSeriesCache()); series.forEach(s -> b2.build(s, reportingPeriod, get(AggregationConfig.class).getAggregation())); } }