1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-13 07:47:14 +03:00

Add support for charting Cmdr statistics.

This commit is contained in:
Jonathan Harris 2015-07-22 04:12:27 +01:00
parent a9a7305ffa
commit 47bccc0684
7 changed files with 152 additions and 126 deletions

View File

@ -23,6 +23,7 @@ import eddn
import loadout
import flightlog
import stats
import chart
import prefs
from config import appname, applongname, config
@ -202,10 +203,20 @@ class AppWindow:
self.status['text'] = "Where are you?!" # Shouldn't happen
elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name','').strip():
self.status['text'] = "What are you flying?!" # Shouldn't happen
elif (config.getint('output') & config.OUT_EDDN) and data['commander'].get('docked') and not data['lastStarport'].get('ships') and not retrying:
# API is flakey about shipyard info - retry if missing (<1s is usually sufficient - 4s for margin).
# API is flakey about shipyard info - retry if missing (<1s is usually sufficient - 5s for margin).
self.w.after(shipyard_retry * 1000, lambda:self.getandsend(retrying=True))
# Stuff we can do while waiting for retry
if config.getint('output') & config.OUT_STAT:
chart.export(data)
if config.getint('output') & config.OUT_LOG:
flightlog.export(data)
if config.getint('output') & config.OUT_SHIP:
loadout.export(data)
return
else:
if __debug__ and retrying: print data['lastStarport'].get('ships') and 'Retry for shipyard - Success' or 'Retry for shipyard - Fail'
@ -213,10 +224,14 @@ class AppWindow:
if __debug__: # Recording
with open('%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h:
h.write(json.dumps(data, indent=2, sort_keys=True))
if config.getint('output') & config.OUT_LOG:
flightlog.export(data)
if config.getint('output') & config.OUT_SHIP:
loadout.export(data)
if not retrying:
if config.getint('output') & config.OUT_STAT:
chart.export(data)
if config.getint('output') & config.OUT_LOG:
flightlog.export(data)
if config.getint('output') & config.OUT_SHIP:
loadout.export(data)
if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)):
# no further output requested

View File

@ -51,6 +51,9 @@
<Component Guid="{433C38E1-F736-4546-AA83-FCD8B0AAA39B}">
<File KeyPath="yes" Source="SourceDir\_ctypes.pyd" />
</Component>
<Component Guid="{BFF92B3F-2F13-4892-87A0-49F8DDF54A60}">
<File KeyPath="yes" Source="SourceDir\_elementtree.pyd" />
</Component>
<Component Guid="{45803711-A2A6-4DA8-8219-F625DE6DB33E}">
<File KeyPath="yes" Source="SourceDir\_hashlib.pyd" />
</Component>
@ -78,6 +81,9 @@
<Component Guid="{A18814B6-B491-42AB-A433-2AD66A823AD7}">
<File KeyPath="yes" Source="SourceDir\library.zip" />
</Component>
<Component Guid="{017F51FB-BC1F-43C9-8089-6E60005F6989}">
<File KeyPath="yes" Source="SourceDir\lxml.etree.pyd" />
</Component>
<Component Guid="{87A99AAA-792F-4092-9D00-5106D99D00AD}">
<File KeyPath="yes" Source="SourceDir\pyexpat.pyd" />
</Component>
@ -318,6 +324,7 @@
<Feature Id='Complete' Level='1'>
<ComponentRef Id="MainExecutable" />
<ComponentRef Id="_ctypes.pyd" />
<ComponentRef Id="_elementtree.pyd" />
<ComponentRef Id="_hashlib.pyd" />
<ComponentRef Id="_socket.pyd" />
<ComponentRef Id="_ssl.pyd" />
@ -327,6 +334,7 @@
<ComponentRef Id="EDMarketConnector.ico" />
<ComponentRef Id="EDMarketConnector.VisualElementsManifest.xml" />
<ComponentRef Id="library.zip" />
<ComponentRef Id="lxml.etree.pyd" />
<ComponentRef Id="pyexpat.pyd" />
<ComponentRef Id="python27.dll" />
<ComponentRef Id="select.pyd" />

View File

@ -5,12 +5,14 @@ This app downloads commodity market and other data from the game [Elite: Dangero
* sends the data to the [Elite Dangerous Data Network](http://eddn-gateway.elite-markets.net/) ("EDDN") from where you and others can use it via online trading tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), etc.
* saves the data to files on your computer that you can load into trading tools such as [Slopey's BPC Market Tool](https://forums.frontier.co.uk/showthread.php?t=76081), [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Inara](http://inara.cz), etc.
* saves a record of your ship loadout and/or flight log.
* saves a record of your ship loadout, flight log and/or statistics.
The user-interface is deliberately minimal - when you land at a station just switch to the app and press the "Update" button or press Enter to automatically download and transmit and/or save your choice of data.
![Windows screenshot](img/win.png) ![Mac screenshot](img/mac.png)
![Balance screenshot](img/Balance.png)
Installation
--------
@ -60,6 +62,9 @@ This app can save a variety of data in a variety of formats:
* Flight log
* Adds a record of your location, ship and cargo to a file that you can open in a text editor or a spreadsheet program such as Excel. Note: Don't edit, rename or move this file - take a copy if you wish to change it.
* Cmdr statstics
* Generates a number of charts of your progress in an Excel file. Note: Don't edit, rename or move this file.
By default these files will be placed in your Documents folder. Since this app will create a lot of files if you use it for a while you may wish to create a separate folder for the files and tell the app to place them there.
Statistics
@ -100,6 +105,8 @@ Linux:
* Requires the Python "imaging-tk", "iniparse" and "requests" modules. On Debian-based systems install these with `sudo apt-get install python-imaging-tk python-iniparse python-requests` .
* Run with `./EDMarketConnector.py` .
On all platforms charting Cmdr statistics additionally requires verison 2.3.0 or later of the [openpyxl](https://pypi.python.org/pypi/openpyxl) module.
Packaging for distribution
--------

215
trends.py → chart.py Executable file → Normal file
View File

@ -1,92 +1,45 @@
#!/usr/bin/python
#
# Creates an Excel spreadsheet graphing player stats using data from .json dumps created by
# EDMarketconnector in interactive mode.
# Creates an Excel spreadsheet graphing player stats
#
# Requires XlsxWriter
# Requires openpyxl >= 2.3
#
import json
import os
import re
try:
import lxml._elementpath # Explicit dependency for py2exe
import openpyxl
if map(int, openpyxl.__version__.split('.')[:2]) < [2,3]:
raise ImportError()
have_openpyxl = True
except:
have_openpyxl = False
import datetime
import xlsxwriter
import time
import re
workbook = xlsxwriter.Workbook('trends.xlsx')
F_TITLE = workbook.add_format({'align': 'center', 'bold':True})
F_SUB = workbook.add_format({'align': 'right', 'bold':True})
F_DATE = workbook.add_format({'num_format': 'yy-mm-dd hh:mm:ss'})
def makesheet(workbook, name, titles):
worksheet = workbook.add_worksheet(name)
worksheet.write(0, 0, 'Date', F_SUB)
if isinstance(titles[0], tuple):
start = end = 1
for (head, subtitles) in titles:
for i in range(len(subtitles)):
worksheet.write(1, end, subtitles[i], F_SUB)
end += 1
worksheet.merge_range(0, start, 0, end-1, head, F_TITLE)
start = end
worksheet.set_column(0, end, 15)
else:
worksheet.set_column(0, len(titles), 15)
for i in range(len(titles)):
worksheet.write(0, i+1, titles[i], F_SUB)
return worksheet
def addrow(worksheet, row, dt, items):
worksheet.write_datetime(row, 0, dt, F_DATE)
for i in range(len(items)):
worksheet.write(row, i+1, items[i])
def makechart(workbook, worksheet, title, axes=None):
chart = workbook.add_chart({'type': 'scatter', 'subtype': 'straight'})
chart.set_title({'name': title})
chart.set_size({'width': 2400, 'height': 1600})
chart.set_x_axis({'date_axis': True, 'num_format': 'yyyy-mm-dd'})
if axes:
if isinstance(axes, list) or isinstance(axes, tuple):
chart.set_y_axis( {'name': axes[0]})
chart.set_y2_axis({'name': axes[1]})
else:
chart.set_y_axis( {'name': axes})
worksheet.insert_chart('B2', chart)
return chart
inputs = {}
regexp = re.compile('.+\.(\d\d\d\d\-\d\d\-\d\dT\d\d\.\d\d\.\d\d)\.json$')
for f in os.listdir('.'):
match = regexp.match(f)
if match:
inputs[datetime.datetime.strptime(match.group(1), '%Y-%m-%dT%H.%M.%S')] = json.loads(open(f).read())
if not inputs:
print "No data!"
exit()
from os.path import isfile, join
from config import config
dataseries = [
{
'name': 'Combat',
'axes': ['qty', 'CR'],
'axes': ['Quantity', 'Profit [CR]'],
'keys': [
('Bounties', ['stats', 'combat', 'bounty', 'qty']),
('Profit from bounties', ['stats', 'combat', 'bounty', 'value'], True),
('Bonds', ['stats', 'combat', 'bond', 'qty']),
('Profit from bonds', ['stats', 'combat', 'bond', 'value'], True),
('Assassin', ['stats', 'missions', 'assassin', 'missionsCompleted']),
('Assassinations', ['stats', 'missions', 'assassin', 'missionsCompleted']),
('Profit from assassin', ['stats', 'missions', 'assassin', 'creditsEarned'], True),
('Hunting', ['stats', 'missions', 'bountyHunter', 'missionsCompleted']),
('Profit from hunting', ['stats', 'missions', 'bountyHunter', 'creditsEarned'], True),
],
],
},
{
'name': 'Trade',
'axes': ['qty', 'CR'],
'axes': ['Quantity', 'Profit [CR]'],
'keys': [
('Profit from trading', ['stats', 'trade', 'profit'], True),
('Commodities traded', ['stats', 'trade', 'qty']),
@ -98,8 +51,8 @@ dataseries = [
],
},
{
'name': 'Explore',
'axes': ['qty', 'CR'],
'name': 'Explorer',
'axes': ['Quantity', 'Profit [CR]'],
'keys': [
('Profits from exploration', ['stats', 'explore', 'creditsEarned'], True),
('Discovery scans', ['stats', 'explore', 'scanSoldLevels', 'lev_0']),
@ -111,7 +64,7 @@ dataseries = [
},
{
'name': 'Crime',
'axes': ['qty', 'CR'],
'axes': ['Quantity', 'Profit [CR]'],
'keys': [
('Fines', ['stats', 'crime', 'fine', 'qty']),
('Lifetime fine value', ['stats', 'crime', 'fine', 'value'], True),
@ -125,17 +78,19 @@ dataseries = [
},
{
'name': 'NPC',
'axes': 'Quantity',
'prefix': ['stats', 'NPC', 'kills', 'ranks'],
'keys': [('Harmless', 'r0'), ('Mostly Harmless', 'r1'), ('Novice', 'r2'), ('Competent', 'r3'), ('Expert', 'r4'), ('Master', 'r5'), ('Dangerous', 'r6'), ('Deadly', 'r7'), ('Elite', 'r8'), ('Capital', 'rArray')],
},
{
'name': 'PVP',
'axes': 'Quantity',
'prefix': ['stats', 'PVP', 'kills', 'ranks'],
'keys': [('Harmless', 'r0'), ('Mostly Harmless', 'r1'), ('Novice', 'r2'), ('Competent', 'r3'), ('Expert', 'r4'), ('Master', 'r5'), ('Dangerous', 'r6'), ('Deadly', 'r7'), ('Elite', 'r8'), ('Capital', 'rArray')],
},
{
'name': 'Balance',
'axes': ['qty', 'CR'],
'axes': ['Quantity', '[CR]'],
'keys': [
('Current balance', ['commander', 'credits'], True),
('Spent on ships', ['stats', 'ship', 'spend', 'ships'], True),
@ -153,63 +108,99 @@ dataseries = [
]
for thing in dataseries:
if isinstance(thing['keys'][0], tuple):
def export(data, csv=False):
if not have_openpyxl: return False
TITLE_F = openpyxl.styles.Font(bold=True)
TITLE_A = openpyxl.styles.Alignment(horizontal='right')
querytime = config.getint('querytime') or int(time.time())
filename = join(config.get('outdir'), 'Cmdr %s.xlsx' % re.sub(r'[\\/:*?"<>|]', '_', data['commander']['name']))
if not isfile(filename):
wb = openpyxl.Workbook()
try:
wb.active.title = 'Combat' # Workbook is created with one sheet - rename it
except:
pass # except that it isn't under 2.30b1
else:
wb = openpyxl.load_workbook(filename)
for thing in dataseries:
legends = [x[0] for x in thing['keys']]
keys = [x[1] for x in thing['keys']]
if thing.get('axes') and (isinstance(thing['axes'], list) or isinstance(thing['axes'], tuple)):
if thing.get('axes') and isinstance(thing['axes'], (list, tuple)):
y2_axis = [len(x)>2 and x[2] for x in thing['keys']]
else:
y2_axis = [False] * len(keys)
else:
legends = keys = thing['keys']
y2_axis = [False] * len(keys)
y2_axis = None
sheet = makesheet(workbook, thing['name'], legends)
if thing['name'] in wb:
ws = wb[thing['name']]
else:
ws = wb.create_sheet(title=thing['name'])
timeseries = sorted(inputs)
for i in range(len(inputs)):
row = i+1
dt = timeseries[i]
data = inputs[dt]
# Add header row
if ws.max_row <= 1: # Returns 1 for empty sheet
ws.append(['Date'] + legends)
for i in range(ws.max_column):
ws.column_dimensions[openpyxl.utils.get_column_letter(i+1)].width = 17
for row in ws.get_squared_range(1, 1, ws.max_column, 1):
for cell in row:
cell.font = TITLE_F
cell.alignment = TITLE_A
# Add data row
vals = [datetime.datetime.fromtimestamp(querytime)]
mydata = data
if thing.get('prefix'):
for key in thing['prefix']:
data = data[key]
vals = []
mydata = mydata[key]
for key2 in keys:
if isinstance(key2, basestring):
vals.append(data.get(key2, 0))
vals.append(mydata.get(key2, 0))
else:
value = data
value = mydata
for key in key2:
value = value.get(key, 0)
if not value: break
vals.append(value)
ws.append(vals)
ws.cell(row=ws.max_row, column=1).number_format = 'yyyy-mm-dd hh:mm:ss' # just a string, not a style
addrow(sheet, row, dt, vals)
dates = openpyxl.chart.Reference(ws, 1, 2, 1, ws.max_row)
chart = makechart(workbook, sheet, thing['name'], thing.get('axes'))
for i in range(len(thing['keys'])):
chart.add_series({'categories': [thing['name'], 1, 0, row, 0],
'values': [thing['name'], 1, 1+i, row, 1+i],
'name': legends[i],
'marker': {'type': 'diamond'},
'y2_axis': y2_axis[i],
})
# Label each line
if y2_axis[i]:
chart.add_series({'categories': [thing['name'], row, 0, row, 0], # last row
'values': [thing['name'], row, 1+i, row, 1+i],
'name': legends[i],
'data_labels': {'series_name': True, 'position': 'right'},
'y2_axis': True,
})
else:
chart.add_series({'categories': [thing['name'], 1, 0, 1, 0], # first row
'values': [thing['name'], 1, 1+i, 1, 1+i],
'name': legends[i],
'data_labels': {'series_name': True, 'position': 'left'},
})
chart.set_legend({'delete_series': range(1, 2*len(thing['keys']), 2)})
chart = openpyxl.chart.ScatterChart()
chart.title = thing['name']
chart.width, chart.height = 60, 30 # in cm!
chart.scatterStyle = 'lineMarker'
chart.set_categories(dates)
chart.x_axis.number_format = ('yyyy-mm-dd') # date only
chart.x_axis.majorGridlines = None
workbook.close()
if y2_axis:
chart.y_axis.majorGridlines = None # prefer grid lines on secondary axis
chart2 = openpyxl.chart.ScatterChart()
chart2.scatterStyle = 'lineMarker'
# Hack - second chart must have different axis ID
chart2.y_axis = openpyxl.chart.axis.NumericAxis(axId=30, crossAx=10, axPos='r', crosses='max')
for i in range(len(keys)):
series = openpyxl.chart.Series(openpyxl.chart.Reference(ws, i+2, 1, i+2, ws.max_row), dates, title_from_data=True)
series.marker.symbol = 'diamond'
if y2_axis and y2_axis[i]:
series.dLbls = openpyxl.chart.label.DataLabels([openpyxl.chart.label.DataLabel(idx=ws.max_row-2, dLblPos='r', showSerName=True)])
chart2.series.append(series)
else:
series.dLbls = openpyxl.chart.label.DataLabels([openpyxl.chart.label.DataLabel(idx=0, dLblPos='l', showSerName=True)])
chart.series.append(series)
if y2_axis:
chart.y_axis.title = thing['axes'][0]
chart2.y_axis.title = thing['axes'][1]
chart.z_axis = chart2.y_axis
chart += chart2
elif thing['axes']:
chart.y_axis.title = thing['axes']
ws.add_chart(chart, 'B2')
wb.save(filename)

View File

@ -71,6 +71,7 @@ class Config:
OUT_CSV = 8
OUT_SHIP = 16
OUT_LOG = 32
OUT_STAT = 64
if platform=='darwin':

BIN
img/Balance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -9,6 +9,7 @@ import ttk
import tkFileDialog
from config import config
from chart import have_openpyxl
if platform=='win32':
@ -77,10 +78,10 @@ class PreferencesDialog(tk.Toplevel):
outframe.grid(padx=10, pady=10, sticky=tk.NSEW)
outframe.columnconfigure(0, weight=1)
output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP)
output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP | config.OUT_STAT)
ttk.Label(outframe, text="Please choose what data to save").grid(row=0, columnspan=2, padx=5, pady=3, sticky=tk.W)
self.out_eddn= tk.IntVar(value = (output & config.OUT_EDDN) and 1 or 0)
ttk.Checkbutton(outframe, text="Send market data to the Elite Dangerous Data Network", variable=self.out_eddn).grid(row=1, columnspan=2, padx=5, sticky=tk.W)
ttk.Checkbutton(outframe, text="Send station data to the Elite Dangerous Data Network", variable=self.out_eddn).grid(row=1, columnspan=2, padx=5, sticky=tk.W)
self.out_bpc = tk.IntVar(value = (output & config.OUT_BPC ) and 1 or 0)
ttk.Checkbutton(outframe, text="Market data in Slopey's BPC format", variable=self.out_bpc, command=self.outvarchanged).grid(row=2, columnspan=2, padx=5, sticky=tk.W)
self.out_td = tk.IntVar(value = (output & config.OUT_TD ) and 1 or 0)
@ -89,14 +90,17 @@ class PreferencesDialog(tk.Toplevel):
ttk.Checkbutton(outframe, text="Market data in CSV format", variable=self.out_csv, command=self.outvarchanged).grid(row=4, columnspan=2, padx=5, sticky=tk.W)
self.out_ship= tk.IntVar(value = (output & config.OUT_SHIP) and 1 or 0)
ttk.Checkbutton(outframe, text="Ship loadout in E:D Shipyard format", variable=self.out_ship, command=self.outvarchanged).grid(row=5, columnspan=2, padx=5, sticky=tk.W)
self.out_log = tk.IntVar(value = (output & config.OUT_LOG) and 1 or 0)
self.out_log = tk.IntVar(value = (output & config.OUT_LOG ) and 1 or 0)
ttk.Checkbutton(outframe, text="Flight log", variable=self.out_log, command=self.outvarchanged).grid(row=6, columnspan=2, padx=5, sticky=tk.W)
ttk.Label(outframe, text=(platform=='darwin' and 'Where:' or 'File location:')).grid(row=7, padx=5, pady=(5,0), sticky=tk.NSEW)
self.out_stat= tk.IntVar(value = have_openpyxl and (output & config.OUT_STAT) and 1 or 0)
ttk.Checkbutton(outframe, text="Cmdr statistics", variable=self.out_stat, command=self.outvarchanged, state=have_openpyxl and tk.NORMAL or tk.DISABLED).grid(row=7, columnspan=2, padx=5, sticky=tk.W)
ttk.Label(outframe, text=(platform=='darwin' and 'Where:' or 'File location:')).grid(row=8, padx=5, pady=(5,0), sticky=tk.NSEW)
self.outbutton = ttk.Button(outframe, text=(platform=='darwin' and 'Change...' or 'Browse...'), command=self.outbrowse)
self.outbutton.grid(row=7, column=1, padx=5, pady=(5,0), sticky=tk.NSEW)
self.outbutton.grid(row=8, column=1, padx=5, pady=(5,0), sticky=tk.NSEW)
self.outdir = ttk.Entry(outframe)
self.outdir.insert(0, config.get('outdir'))
self.outdir.grid(row=8, columnspan=2, padx=5, pady=5, sticky=tk.EW)
self.outdir.grid(row=9, columnspan=2, padx=5, pady=5, sticky=tk.EW)
self.outvarchanged()
privacyframe = ttk.LabelFrame(frame, text='Privacy')
@ -123,7 +127,7 @@ class PreferencesDialog(tk.Toplevel):
#self.wait_window(self) # causes duplicate events on OSX
def outvarchanged(self):
local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship.get() or self.out_log.get()
local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship.get() or self.out_log.get() or self.out_stat.get()
self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED
self.outdir['state'] = local and 'readonly' or tk.DISABLED
@ -162,7 +166,7 @@ class PreferencesDialog(tk.Toplevel):
credentials = (config.get('username'), config.get('password'))
config.set('username', self.username.get().strip())
config.set('password', self.password.get().strip())
config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship.get() and config.OUT_SHIP or 0) + (self.out_log.get() and config.OUT_LOG or 0))
config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship.get() and config.OUT_SHIP or 0) + (self.out_log.get() and config.OUT_LOG or 0) + (self.out_stat.get() and config.OUT_STAT or 0))
config.set('outdir', self.outdir.get().strip())
config.set('anonymous', self.out_anon.get())
self.destroy()