Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow a variety of data series for statement of assets #3754

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,6 +1,9 @@
package name.abuchen.portfolio.ui.views.dataseries;

import java.util.Arrays;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;

import org.eclipse.swt.graphics.Image;
Expand Down Expand Up @@ -40,6 +43,32 @@ public enum ClientDataSeries
EARNINGS, EARNINGS_ACCUMULATED, FEES, FEES_ACCUMULATED;
}

public static final Map<ClientDataSeries, String> statementOfAssetsDataSeriesLabels = new EnumMap<>(ClientDataSeries.class)
{
private static final long serialVersionUID = 1319016001158914537L;

{
put(ClientDataSeries.TOTALS, Messages.LabelTotalSum);
put(ClientDataSeries.TRANSFERALS, Messages.LabelTransferals);
put(ClientDataSeries.INVESTED_CAPITAL, Messages.LabelInvestedCapital);
put(ClientDataSeries.ABSOLUTE_INVESTED_CAPITAL, Messages.LabelAbsoluteInvestedCapital);
put(ClientDataSeries.ABSOLUTE_DELTA, Messages.LabelDelta);
put(ClientDataSeries.ABSOLUTE_DELTA_ALL_RECORDS, Messages.LabelAbsoluteDelta);
put(ClientDataSeries.TAXES, Messages.ColumnTaxes);
put(ClientDataSeries.TAXES_ACCUMULATED, Messages.LabelAccumulatedTaxes);
put(ClientDataSeries.DIVIDENDS, Messages.LabelDividends);
put(ClientDataSeries.DIVIDENDS_ACCUMULATED, Messages.LabelAccumulatedDividends);
put(ClientDataSeries.INTEREST, Messages.LabelInterest);
put(ClientDataSeries.INTEREST_ACCUMULATED, Messages.LabelAccumulatedInterest);
put(ClientDataSeries.INTEREST_CHARGE, Messages.LabelInterestCharge);
put(ClientDataSeries.INTEREST_CHARGE_ACCUMULATED, Messages.LabelAccumulatedInterestCharge);
put(ClientDataSeries.EARNINGS, Messages.LabelEarnings);
put(ClientDataSeries.EARNINGS_ACCUMULATED, Messages.LabelAccumulatedEarnings);
put(ClientDataSeries.FEES, Messages.LabelFees);
put(ClientDataSeries.FEES_ACCUMULATED, Messages.LabelFeesAccumulated);
}
};

/**
* Type of objects for which the PerformanceIndex is calculated.
*/
Expand All @@ -52,6 +81,7 @@ public enum Type
ACCOUNT("Account", i -> ((Account) i).getUUID()), //$NON-NLS-1$
ACCOUNT_PRETAX("Account-PreTax", i -> ((Account) i).getUUID()), //$NON-NLS-1$
PORTFOLIO("Portfolio", i -> ((Portfolio) i).getUUID()), //$NON-NLS-1$
TYPE_PARENT("Type-Parent-", i -> ((GroupedDataSeries) i).getId()), //$NON-NLS-1$
PORTFOLIO_PRETAX("Portfolio-PreTax", i -> ((Portfolio) i).getUUID()), //$NON-NLS-1$
PORTFOLIO_PLUS_ACCOUNT("[+]Portfolio", i -> ((Portfolio) i).getUUID()), //$NON-NLS-1$
PORTFOLIO_PLUS_ACCOUNT_PRETAX("[+]Portfolio-PreTax", i -> ((Portfolio) i).getUUID()), //$NON-NLS-1$
Expand All @@ -75,7 +105,7 @@ String buildUUID(Object instance)
}

private Type type;
private Object group;
private Object[] groups;
private Object instance;
private String label;
private boolean isLineChart = true;
Expand All @@ -98,10 +128,19 @@ String buildUUID(Object instance)
this(type, null, instance, label, color);
}

/* package */ DataSeries(Type type, Object[] groups, Object instance, String label, RGB color)
{
this.type = type;
this.groups = groups;
this.instance = instance;
this.label = label;
this.color = color;
}

/* package */ DataSeries(Type type, Object group, Object instance, String label, RGB color)
{
this.type = type;
this.group = group;
this.groups = group != null ? Arrays.asList(group).toArray() : null;
this.instance = instance;
this.label = label;
this.color = color;
Expand All @@ -112,9 +151,9 @@ public Type getType()
return type;
}

public Object getGroup()
public Object[] getGroups()
{
return group;
return groups;
}

public Object getInstance()
Expand All @@ -124,6 +163,9 @@ public Object getInstance()

public String getLabel()
{
if (instance instanceof GroupedDataSeries c && groups.length > 0)
return groups[groups.length - 1] + " - " + label; //$NON-NLS-1$

Comment on lines +166 to +168
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is using the "group" essentially as second label.
I understand that we need two labels (in the selection dialog the full label does not make sense, in the chart legend it is required to understand the data series).
I think we should make it then explicit and not the logic that the last element in the object array is a label.

return isBenchmark() ? label + " " + Messages.ChartSeriesBenchmarkSuffix : label; //$NON-NLS-1$
}

Expand Down
Expand Up @@ -102,6 +102,11 @@ private PerformanceIndex calculate(DataSeries series, Interval reportingPeriod)
return PerformanceIndex.forPortfolio(client, converter, (Portfolio) series.getInstance(),
reportingPeriod, warnings);

case TYPE_PARENT:
var instance = ((GroupedDataSeries) series.getInstance());

return instance.getPerformanceIndexMethod(client, converter, reportingPeriod, warnings);

case PORTFOLIO_PRETAX:
return calculatePortfolioPretax(series, reportingPeriod, warnings);

Expand Down
@@ -1,10 +1,12 @@
package name.abuchen.portfolio.ui.views.dataseries;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.layout.GridDataFactory;
Expand Down Expand Up @@ -32,7 +34,6 @@
import name.abuchen.portfolio.ui.Images;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.util.viewers.CopyPasteSupport;
import name.abuchen.portfolio.ui.views.dataseries.DataSeries.Type;

public class DataSeriesSelectionDialog extends Dialog
{
Expand Down Expand Up @@ -138,18 +139,48 @@ public void setElements(List<DataSeries> elements)
Node child = new Node(series.getSearchLabel());
child.dataSeries = series;

Node parent = type2node.computeIfAbsent(map(series.getType()), Node::new);
Node parent = type2node.computeIfAbsent(map(series), Node::new);

if (series.getGroup() != null)
if (series.getGroups() != null)
buchen marked this conversation as resolved.
Show resolved Hide resolved
{
Node group = group2node.computeIfAbsent(series.getGroup(), g -> {
Node n = new Node(g.toString());
n.parent = parent;
parent.children.add(n);
return n;
});
child.parent = group;
group.children.add(child);
Node lastParentTracker = parent;
int index = 0;

for (Object groupGiven : series.getGroups())
{
final Node lastParent = lastParentTracker;

String groupPath = Arrays.asList(series.getGroups())
.stream()
.map(Object::toString)
.limit(index + 1)
.collect(Collectors.joining("|")); //$NON-NLS-1$

groupPath = map(series) + "|" + groupPath; //$NON-NLS-1$

Node group = group2node.computeIfAbsent(groupPath, g -> {
Node n = new Node(groupGiven.toString());
n.parent = lastParent;
lastParent.children.add(n);
return n;
});

// If there are no more nested groups to go through
// we set the data series as the children
if (series.getGroups().length == (index + 1))
{
group.children.add(child);
child.parent = group;
}
else
{
// If there are nested groups, we just set the current
// node ready to be the parent
lastParentTracker = group;
}

index++;
}
}
else
{
Expand All @@ -165,10 +196,13 @@ public void setElements(List<DataSeries> elements)
* Reduce number of first-level folders to a meaningful set for the
* end-user.
*/
private String map(Type type)
private String map(DataSeries dataSeries)
{
switch (type)

switch (dataSeries.getType())
{
case TYPE_PARENT:
return ((GroupedDataSeries) dataSeries.getInstance()).getTopLevelLabel();
case SECURITY:
return Messages.LabelSecurities;
case SECURITY_BENCHMARK:
Expand Down Expand Up @@ -269,8 +303,6 @@ public String getText(Object element)

hookListener();

treeViewer.expandAll();

return composite;
}

Expand Down
Expand Up @@ -35,8 +35,8 @@ public DataSeriesSet(Client client, IPreferenceStore preferences, DataSeries.Use
switch (useCase)
{
case STATEMENT_OF_ASSETS:
buildStatementOfAssetsDataSeries();
break;
buildStatementOfAssetsDataSeries(client, preferences, wheel);
return;
Comment on lines +38 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return statement prevents existing "common" series to be added.
We must not break existing diagram configurations.
Users have spent considerable time to setup their diagrams and they should not break.
Also, the upcoming mobile application is also supporting the existing modifiers.

Let's discuss in the comments of the pull request how to achieve backwards compatibility.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can add a DataSeries property for whether it's "visible"? And then for the returned DataSeriesSet for Statement of Assets, set it to false so the old ones don't show?

case PERFORMANCE:
buildPerformanceDataSeries(client, preferences, wheel);
break;
Expand Down Expand Up @@ -68,89 +68,81 @@ public DataSeries lookup(String uuid)
return availableSeries.stream().filter(d -> d.getUUID().equals(uuid)).findAny().orElse(null);
}

private void buildStatementOfAssetsDataSeries()
private void buildStatementOfAssetsDataSeries(Client client, IPreferenceStore preferences, ColorWheel wheel)
{
availableSeries.add(new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.TOTALS, Messages.LabelTotalSum,
Colors.TOTALS.getRGB()));

DataSeries series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.TRANSFERALS,
Messages.LabelTransferals, Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY).getRGB());
series.setLineChart(false);
availableSeries.add(series);
for (var entry : DataSeries.statementOfAssetsDataSeriesLabels.entrySet())
{
// Common folder
availableSeries.add(new DataSeries(DataSeries.Type.CLIENT, entry.getKey(), entry.getValue(), wheel.next()));

for (Portfolio portfolio : client.getPortfolios())
{
var instance = new GroupedDataSeries(portfolio, entry.getKey(), DataSeries.Type.PORTFOLIO_PLUS_ACCOUNT);
instance.setIsPortfolioPlusReferenceAccount(true);

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.INVESTED_CAPITAL,
Messages.LabelInvestedCapital, Display.getDefault().getSystemColor(SWT.COLOR_GRAY).getRGB());
series.setShowArea(true);
availableSeries.add(series);
var name = portfolio.getName() + " + " + portfolio.getReferenceAccount().getName(); //$NON-NLS-1$

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.ABSOLUTE_INVESTED_CAPITAL,
Messages.LabelAbsoluteInvestedCapital,
Display.getDefault().getSystemColor(SWT.COLOR_GRAY).getRGB());
series.setShowArea(true);
availableSeries.add(series);
var dataSeries = new DataSeries(DataSeries.Type.TYPE_PARENT, name, instance, entry.getValue(),
wheel.next());

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.ABSOLUTE_DELTA, Messages.LabelDelta,
Display.getDefault().getSystemColor(SWT.COLOR_GRAY).getRGB());
availableSeries.add(series);
availableSeries.add(dataSeries);
}

for (Portfolio portfolio : client.getPortfolios())
{
var instance = new GroupedDataSeries(portfolio, entry.getKey(), DataSeries.Type.PORTFOLIO);

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.ABSOLUTE_DELTA_ALL_RECORDS,
Messages.LabelAbsoluteDelta, Display.getDefault().getSystemColor(SWT.COLOR_GRAY).getRGB());
availableSeries.add(series);
var dataSeries = new DataSeries(DataSeries.Type.TYPE_PARENT, portfolio.getName(), instance,
entry.getValue(), wheel.next());

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.TAXES, Messages.ColumnTaxes,
Display.getDefault().getSystemColor(SWT.COLOR_RED).getRGB());
series.setLineChart(false);
availableSeries.add(series);
availableSeries.add(dataSeries);
}

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.TAXES_ACCUMULATED,
Messages.LabelAccumulatedTaxes, Display.getDefault().getSystemColor(SWT.COLOR_RED).getRGB());
availableSeries.add(series);
for (Account account : client.getAccounts())
{
var instance = new GroupedDataSeries(account, entry.getKey(), DataSeries.Type.ACCOUNT);

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.DIVIDENDS, Messages.LabelDividends,
Display.getDefault().getSystemColor(SWT.COLOR_DARK_MAGENTA).getRGB());
series.setLineChart(false);
availableSeries.add(series);
var dataSeries = new DataSeries(DataSeries.Type.TYPE_PARENT, account.getName(), instance,
entry.getValue(), wheel.next());

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.DIVIDENDS_ACCUMULATED,
Messages.LabelAccumulatedDividends,
Display.getDefault().getSystemColor(SWT.COLOR_DARK_MAGENTA).getRGB());
availableSeries.add(series);
availableSeries.add(dataSeries);
}

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.INTEREST, Messages.LabelInterest,
Colors.DARK_GREEN.getRGB());
series.setLineChart(false);
availableSeries.add(series);
for (Taxonomy taxonomy : client.getTaxonomies())
{
taxonomy.foreach(new Taxonomy.Visitor()
{
@Override
public void visit(Classification classification)
{
if (classification.getParent() == null)
return;

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.INTEREST_ACCUMULATED,
Messages.LabelAccumulatedInterest, Colors.DARK_GREEN.getRGB());
availableSeries.add(series);
Object[] groups = { taxonomy, classification.getPathName(false) };

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.INTEREST_CHARGE, Messages.LabelInterestCharge,
Colors.DARK_GREEN.getRGB());
series.setLineChart(false);
availableSeries.add(series);
var instance = new GroupedDataSeries(classification, entry.getKey(),
DataSeries.Type.CLASSIFICATION);

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.INTEREST_CHARGE_ACCUMULATED,
Messages.LabelAccumulatedInterestCharge, Colors.DARK_GREEN.getRGB());
availableSeries.add(series);
var dataSeries = new DataSeries(DataSeries.Type.TYPE_PARENT, groups, instance, entry.getValue(),
wheel.next());

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.EARNINGS, Messages.LabelEarnings,
Colors.DARK_GREEN.getRGB());
series.setLineChart(false);
availableSeries.add(series);
availableSeries.add(dataSeries);
}
});
}

ClientFilterMenu menu = new ClientFilterMenu(client, preferences);

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.EARNINGS_ACCUMULATED,
Messages.LabelAccumulatedEarnings, Colors.DARK_GREEN.getRGB());
availableSeries.add(series);
for (ClientFilterMenu.Item item : menu.getCustomItems())
{
var instance = new GroupedDataSeries(item, entry.getKey(), DataSeries.Type.CLIENT_FILTER);

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.FEES, Messages.LabelFees,
Colors.GRAY.getRGB());
series.setLineChart(false);
availableSeries.add(series);
var dataSeries = new DataSeries(DataSeries.Type.TYPE_PARENT, item.getLabel(), instance,
entry.getValue(), wheel.next());

series = new DataSeries(DataSeries.Type.CLIENT, ClientDataSeries.FEES_ACCUMULATED,
Messages.LabelFeesAccumulated, Colors.GRAY.getRGB());
availableSeries.add(series);
availableSeries.add(dataSeries);
}
}
}

private void buildPerformanceDataSeries(Client client, IPreferenceStore preferences, ColorWheel wheel)
Expand Down