#!/usr/bin/env python
##############################################################################
#
# PDFgui by DANSE Diffraction group
# Simon J. L. Billinge
# (c) 2006 trustees of the Michigan State University.
# All rights reserved.
#
# File coded by: Jiwu Liu
#
# See AUTHORS.txt for a list of people who contributed.
# See LICENSE.txt for license information.
#
##############################################################################
from diffpy.pdfgui.control.controlerrors import ControlConfigError, ControlStatusError
from diffpy.pdfgui.control.pdfcomponent import PDFComponent
from diffpy.pdfgui.gui.extendedplotframe import ExtendedPlotFrame
# Preset plotting style
colors = (
"red",
"blue",
"magenta",
"cyan",
"green",
"yellow", # "black",
"darkRed",
"darkBlue",
"darkMagenta",
"darkCyan",
"darkGreen",
"darkYellow",
)
lines = ("solid", "dash", "dot", "dashDot")
symbols = ("circle", "square", "triangle", "diamond") # ,"cross","xCross")
# this is to map 'r' to what it is supposed to be. For example, when user asks
# for plotting 'Gobs' against 'r', the real data objects are 'Gobs' and 'robs'
transdict = {
"Gobs": "robs",
"Gcalc": "rcalc",
"Gdiff": "rcalc",
"Gtrunc": "rcalc",
"crw": "rcalc",
}
baselineStyle = {
"with": "lines",
"line": "solid",
"color": "black",
"width": 1,
"legend": "_nolegend_",
}
def _transName(name):
"""Translate name of y object.
This is mainly for plotting of parameters. GUI will pass in a integer to
indicate which parameter to be plotted. However, in data storage the
parameter is denoted as '@n'
name -- name of data item
"""
if isinstance(name, int):
rv = "@" + str(name)
else:
rv = str(name)
return rv
def _fullName(dataId):
"""Construct full name."""
from diffpy.pdfgui.control.fitting import Fitting
if hasattr(dataId, "owner") and isinstance(dataId.owner, Fitting):
return _fullName(dataId.owner) + "/" + dataId.name
else:
return dataId.name
def _buildStyle(plotter, name, group, yNames):
"""Trying to figure out a good style.
1. generally we want line style for Gcalc, Gdiff, crw, symbol style for Gobs,
and line-symbol style for the rest
2. there is a preferred style for plotting a single PDF curve
plotter -- A plotter instance
name -- what is to be plotted (y name)
group -- which group the curve is in (group = -1 means it is the only group)
yNames -- all y to be plotted
return: style dictionary
"""
if name in ("Gcalc", "Gdiff", "crw"):
style = plotter.buildLineStyle()
style["line"] = "solid"
elif name in ("Gobs", "Gtrunc"):
style = plotter.buildSymbolStyle()
# Use open circle always
style["symbolColor"] = "white"
style["symbol"] = "circle"
style["symbolSize"] = 6
else:
style = plotter.buildLineSymbolStyle()
style["line"] = "dash"
style["symbol"] = "circle"
style["symbolSize"] = 8
# We only care about how to arrange Gdiff Gobs Gcalc Gtrunc crw nicely
if group < 0:
# use fixed style for single PDFFit picture
if name == "Gcalc":
style["color"] = "red"
elif name in ("Gobs", "Gtrunc"):
style["color"] = "blue"
elif name in ("Gdiff", "crw"):
style["color"] = "green"
else:
# make sure Gdiff, Gtrunc, Gobs, crw are having same color
if name in ("Gobs", "Gtrunc", "Gdiff", "Gcalc", "crw"):
style["color"] = colors[group % len(colors)]
if name == "Gcalc":
# for visual effect, change Gcalc to black if it's going to be plotted against Gobs/Gtrunc
if "Gobs" in yNames or "Gtrunc" in yNames:
style["color"] = "black"
return style
[docs]
def deblank(s):
"""Remove all whitespace from the given string."""
return "".join(s.split())
[docs]
class Plotter(PDFComponent):
"""Plots a single graph.
It can have multiple curves.
"""
__plotWindowNumber = 1
[docs]
class Curve:
"""Curve stores the information for a curve in the plot.
There are three ways of forming x and y data lists.
(1) r and g(r) from a single refinement are vectors by themselves
(2) A scalar data item (any item other than r and g(r)) can form a
vector if multiple timeSteps (refinement steps) are specified.
(3) A scalar data item (any item other than r and g(r)) can form a
vector if multiple refinement (multiple ids) are specified
name -- The curve name
plotwnd -- The window where the curve is drawn
xStr -- Data name (string) for x axis
yStr -- Data name (string) for y axis
steps -- refinement step list
ids -- The list of object ids that the curve is related to
offset -- curve displacement in y direction
style --The drawing style of the curve
xData, yData -- data to be plotted
x, y -- original data for exporting (curve could be shifted)
bMultiData -- if the curve data comes from multiple data objects
bMultiStep -- if the curve data comes from multiple refinement step
ref -- reference of curve in the plot window
initialized -- if curve has been inserted
dataChanged -- if curve data has changed
"""
def __init__(self, name, plotwnd, xStr, yStr, steps, ids, offset, style):
"""initialize.
name -- The curve name
plotwnd -- The window where the curve is drawn
xStr -- Data name (string) for x axis
yStr -- Data name (string) for y axis
steps -- refinement step list
ids -- The list of object ids that the curve is related to
offset -- curve displacement in y direction
style --The drawing style of the curve
"""
self.name = name
self.plotwnd = plotwnd
self.ids = ids
self.steps = steps
self.xStr = xStr
self.yStr = yStr
self.offset = offset
self.style = style
self.bMultiData = len(self.ids) > 1
self.bMultiStep = False
if self.steps is None or isinstance(self.steps, list):
self.bMultiStep = True
self.xData = None
self.yData = None
self.x = None
self.y = None
# Reference to the curve object in the underlying plotting library
self.ref = None
self.initialized = False
self.dataChanged = False
# validate user's choice
self.validate()
[docs]
def validate(self):
"""Validate(self) --> check if the curve is valid.
Validity
is broken:
(1) when xStr or yStr doesn't refer to a legal vector
(2) when sizes of xStr and yStr don't match
"""
bItemIsVector = False
if self.xStr in ("r", "rcalc", "robs"):
if self.yStr not in ("Gobs", "Gcalc", "Gdiff", "Gtrunc", "crw"):
emsg = "x={}, y={} don't match".format(self.xStr, self.yStr)
raise ControlConfigError(emsg)
bItemIsVector = True
elif self.xStr in ("Gobs", "Gcalc", "Gdiff", "Gtrunc", "crw"):
raise ControlConfigError("%s can't be x axis" % self.xStr)
elif self.yStr in ("Gobs", "Gcalc", "Gdiff", "Gtrunc", "crw"):
# Get called when x is not r but y is not Gobs, Gtrunc Gdiff...
raise ControlConfigError("%s can only be plotted against r" % self.yStr)
# There are three booleans
# (1) bItemIsVector
# (2) self.ids has only one element
# (3) self.allSteps
# The logic below make sure only one of them can be true.
if bItemIsVector:
if self.bMultiData or self.bMultiStep:
emsg = ("({}, {}) can't be plotted with multiple " "refinements/steps").format(
self.xStr, self.yStr
)
raise ControlConfigError(emsg)
else:
if not self.bMultiData and not self.bMultiStep:
raise ControlConfigError("(%s, %s) is a single point" % (self.xStr, self.yStr))
elif self.bMultiData and self.bMultiStep:
emsg = (
"({}, {}) can't be plotted with both multiple " "refinements and multiple steps"
).format(self.xStr, self.yStr)
raise ControlConfigError(emsg)
[docs]
def notify(self, changedIds=None, plotwnd=None):
"""Notify Curve object certain data is updated.
changedIds -- objects to which changed data is associated with
"""
if plotwnd:
self.plotwnd = plotwnd
# in the case when changedIds are given explicitly, use it.
if changedIds:
affectedIds = []
for id in self.ids:
for changedId in changedIds:
if id is changedId or id.owner is changedId:
affectedIds.append(id)
break
# If the change doesn't affect any id, do nothing
if not affectedIds:
return False
else:
affectedIds = self.ids
# translation may be required
xStr = self.xStr
if xStr == "r":
xStr = transdict.get(self.yStr, xStr)
if self.bMultiData:
# Local list is maintained here
if self.xData is None:
self.xData = [None] * len(self.ids)
if self.yData is None:
self.yData = [None] * len(self.ids)
for id in affectedIds:
i = self.ids.index(id)
self.yData[i] = id.getData(self.yStr, -1)
if xStr == "step":
raise AssertionError("Can not plot against step")
elif xStr == "index":
self.xData[i] = i
else:
self.xData[i] = id.getData(xStr, -1)
else:
# affectedIds has only one member
if self.bMultiStep:
steps = None # None to get the whole steps
else:
steps = -1 #
# plot multiple refinement steps for a single dataId
# in deed, the reference is not gonna change
self.yData = affectedIds[0].getData(self.yStr, steps)
if xStr == "step":
if self.yData is None:
self.xData = None
else:
self.xData = list(range(len(self.yData)))
else:
self.xData = affectedIds[0].getData(xStr, steps)
self.x = self.xData
self.y = self.yData
def _shift(y):
return y + self.offset
if self.yData and self.offset: # not zero
self.yData = [_shift(yi) for yi in self.yData]
if self.xData and self.yData: # not empty or None
return self.draw()
else:
return False
[docs]
def draw(self):
"""Draw the curve in the graph.
It will make sure the data is OK, and plot to the screen.
"""
if self.bMultiData:
# xs and ys initialize here. They are actual data object to be
# used for plotting
xs = []
ys = []
plotData = sorted(zip(self.xData, self.yData))
for x, y in plotData:
if x is not None and y is not None:
xs.append(x)
ys.append(y)
self.x = xs
self.y = ys
else:
xs = self.xData
ys = self.yData
if not xs or not ys:
return False
# If it can get here, data is ready now.
if self.ref is None:
self.ref = self.plotwnd.insertCurve(xs, ys, self.style)
if self.yStr == "Gdiff":
# add a baseline for any Gdiff
rs = self.ids[0].rcalc
if not rs:
rs = self.ids[0].robs
hMin = min(rs)
hMax = max(rs)
self.plotwnd.insertCurve([hMin, hMax], [self.offset, self.offset], baselineStyle)
else:
# update only
self.plotwnd.updateData(self.ref, xs, ys)
return True
def __init__(self, name=None):
"""initialize.
name -- name of plot
"""
if name is None:
name = "Plot [%i]" % Plotter.__plotWindowNumber
PDFComponent.__init__(self, name)
import threading
self.lock = threading.RLock()
self.curves = []
self.window = None
self.isShown = False
from diffpy.pdfgui.control.pdfguicontrol import pdfguicontrol
self.controlCenter = pdfguicontrol()
# add some flavor by starting with random style
import random
self.symbolStyleIndex = random.randint(0, 100)
self.lineStyleIndex = random.randint(0, 100)
return
[docs]
def close(self, force=True):
"""Close up the plot.
force -- if True, close forcibly
"""
if self.window is not None:
# self.window.Close(True)
self.window.Destroy()
self.window = None
[docs]
def onWindowClose(self):
"""Get called when self.window is closed by user."""
self.window = None
try:
self.controlCenter.plots.remove(self)
except ValueError:
# if controlCenter doesn't know me, I'm just fine to bail out
pass
[docs]
def buildSymbolStyle(self, index=-1):
"""Generate a symbol style.
index -- plotting style index
"""
# To build different symbol style, we first change color then the symbol
i = index
if i == -1:
i = self.symbolStyleIndex
self.symbolStyleIndex += 1
symbolIndex = i % len(symbols)
colorIndex = i % len(colors)
return {
"with": "points",
"color": colors[colorIndex],
"symbolColor": colors[colorIndex],
"symbol": symbols[symbolIndex],
"symbolSize": 3,
}
[docs]
def buildLineStyle(self, index=-1):
"""Generate a line style.
index -- plotting style index
"""
# To build different line style, we first change color then the line
i = index
if i == -1:
i = self.lineStyleIndex
self.lineStyleIndex += 1
lineIndex = i % len(lines)
colorIndex = i % len(colors)
return {
"with": "lines",
"color": colors[colorIndex],
"line": lines[lineIndex],
"width": 2,
}
[docs]
def buildLineSymbolStyle(self, index=-1):
"""Generate a linesymbol style.
index -- plotting style index
"""
style = self.buildLineStyle(index)
style.update(self.buildSymbolStyle(index))
style["with"] = "linespoints"
return style
[docs]
def plot(self, xName, yNames, ids, shift, dry):
"""Make a 2D plot.
xName -- x data item name
yNames -- list of y data item names
ids -- Objects where y data items are taken from
shift -- y spacing for different ids
dry -- dry run
"""
def _addCurve(dataIds):
# Identify the plot type. This is used to automatically modify
# 'Gdiff' and 'crw' in certain types of plots.
yset = set(yNames)
if "Gdiff" in yset:
yset.remove("Gdiff")
if "crw" in yset:
yset.remove("crw")
# add yNames one by one for given dataIds
for y in yNames:
_offset = offset
legend = None
style = None
if not dry:
if len(dataIds) == 1 and group != -1:
# legend = dataIds[0].name + ": " + _transName(y)
legend = _fullName(dataIds[0]) + ": " + _transName(y)
else:
# 1.Group = -1, multiple ids give a single curve
# 2.there is only one dataId so that prefix unneeded
legend = _transName(y)
style = _buildStyle(self, y, group, yNames)
style["legend"] = legend
# automatically apply offset if we're plotting more than
# just 'Gdiff' and 'crw'
if y in ("Gdiff", "crw") and group == -1 and len(yset) > 0:
_offset = shift
# Create curve, get data for it and update it in the plot
curve = Plotter.Curve(legend, self.window, xName, y, step, dataIds, _offset, style)
self.curves.append(curve)
return
if not ids: # empty
raise ControlConfigError("Plotter: No data is selected")
if not yNames:
raise ControlConfigError("Plotter: No y item is selected")
# bSeparateID indicates if we want data from different ID to be
# plotted in different curve or not
bSeparateID = False
if len(ids) > 1 and xName in ("r", "rcalc", "step"):
# multi ID and within each ID we wants a vector, so curve can
# only be plotted separately.
bSeparateID = True
# set up the step
if xName == "step":
step = None
else:
step = -1
self.curves = []
if "Gcalc" in yNames:
yNames.remove("Gcalc")
yNames.append("Gcalc")
# default is no shift, single group.
offset = 0.0
group = -1
if bSeparateID:
for id in ids:
group += 1
_addCurve(
[
id,
]
)
offset += shift
else:
_addCurve(ids)
# clean up, it's only a dry run
if dry:
self.curves = []
return
# Real plot starts
if self.window is None:
# plotWindown may either not be ready or it has been closed
self.window = ExtendedPlotFrame(self.controlCenter.gui)
Plotter.__plotWindowNumber += 1
self.window.plotter = self
else:
self.window.clear()
for curve in self.curves:
# Initial notification, don't plot immediately, wait for last line to be added
# This is to optimize plotting multiple curves.
curve.notify(plotwnd=self.window)
# make the graph title, x, y label
yStrs = [_transName(yName) for yName in yNames]
if yStrs[0].startswith("G"):
# then all are Gs
yLabel = "G"
else:
yLabel = ",".join(yStrs)
title = ""
if len(ids) == 1:
title = ids[0].name + ": "
title += yLabel
self.window.setTitle(self.name + " " + title, title)
self.window.setXLabel(_transName(xName))
self.window.setYLabel(yLabel)
# show the graph
self.window.replot()
self.show(True)
[docs]
def show(self, bShow=None):
"""Show the plot on screen.
bShow -- True to show, False to Hide. None to toggle
return value: current status of window
"""
if self.window is None:
raise ControlStatusError("Plot: %s has no window" % self.name)
if bShow is None:
bShow = not self.isShown
self.window.Show(bShow)
if bShow: # True
# further bring it to top
self.window.Raise()
self.isShown = bShow
return self.isShown
[docs]
def notify(self, data):
"""Change of the data is notified.
data -- data object that has changed
"""
if not self.curves or self.window is None:
return
ret = False
for curve in self.curves:
ret |= curve.notify(
changedIds=[
data,
]
)
if ret:
self.window.replot()
[docs]
def export(self, filename):
"""Export current data to external file.
filename -- the name of the file to save data
"""
# Check if any curve
if len(self.curves) == 0:
return
import getpass
import time
outfile = open(filename, "w")
header = "# Generated on %s by %s.\n" % (time.ctime(), getpass.getuser())
header += "# This file was created by PDFgui.\n"
outfile.write(header)
xylist = [(c.x, c.y) for c in self.curves]
xynames = [(_transName(c.xStr), deblank(c.name)) for c in self.curves]
_exportCompactData(outfile, xylist, xynames)
outfile.close()
return
# End of class Plotter
def _exportCompactData(fp, xylist, xynames=None):
"""Write the xylist data in a text format to the file object fp.
The curves with the same x are grouped in the same datasets.
The datasets are marked with "#S 1", "#S 2", etc. labels according
to the spec format http://www.certif.com/cplot_manual/ch0c_C_11_3.html
fp -- file type object that is writable
xylist -- list of (x, y) tuples of x and y arrays. Items with
empty x or empty y are ignored.
xynames -- list of tuples of (xname, yname) strings. These are
used as a header in the dataset blocks.
No return value.
"""
dataformat = "%g"
# build the default xynames:
if xynames is None:
xynames = [("x%i" % i, "y%i" % i) for i in range(len(xylist))]
datasets = []
datanames = []
xt2idx = {}
for (x, y), (xn, yn) in zip(xylist, xynames):
if x is None or not len(x):
continue
if y is None or not len(y):
continue
xt = tuple(x)
i = xt2idx.setdefault(xt, len(xt2idx))
if not i < len(datasets):
datasets.append([])
datanames.append([])
ds = datasets[i]
dn = datanames[i]
if not ds:
ds.append(x)
dn.append(xn)
ds.append(y)
dn.append(yn)
for i, (ds, dn) in enumerate(zip(datasets, datanames)):
# separate datasets with a blank line:
if i > 0:
fp.write("\n")
fp.write("#S %i\n" % (i + 1))
fp.write("#L %s\n" % (" ".join(dn)))
ncols = len(ds)
fmt = " ".join(ncols * [dataformat]) + "\n"
for cols in zip(*ds):
line = fmt % cols
fp.write(line)
return
# End of file