1#    Copyright 2016-2017 ARM Limited
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15"""Base matplotlib plotter module"""
16from abc import abstractmethod, ABCMeta
17from collections import defaultdict as ddict
18import matplotlib.pyplot as plt
19from trappy.plotter import AttrConf
20from trappy.plotter.Constraint import ConstraintManager
21from trappy.plotter.PlotLayout import PlotLayout
22from trappy.plotter.AbstractDataPlotter import AbstractDataPlotter
23from trappy.plotter.ColorMap import ColorMap
24
25
26
27class StaticPlot(AbstractDataPlotter):
28    """
29    This class uses :mod:`trappy.plotter.Constraint.Constraint` to
30    represent different permutations of input parameters. These
31    constraints are generated by creating an instance of
32    :mod:`trappy.plotter.Constraint.ConstraintManager`.
33
34    :param traces: The input data
35    :type traces: a list of :mod:`trappy.trace.FTrace`,
36        :mod:`trappy.trace.SysTrace`, :mod:`trappy.trace.BareTrace`
37        or :mod:`pandas.DataFrame` or a single instance of them.
38
39    :param column: specifies the name of the column to
40           be plotted.
41    :type column: (str, list(str))
42
43    :param templates: TRAPpy events
44
45        .. note::
46
47                This is not required if a :mod:`pandas.DataFrame` is
48                used
49
50    :type templates: :mod:`trappy.base.Base`
51
52    :param filters: Filter the column to be plotted as per the
53        specified criteria. For Example:
54        ::
55
56            filters =
57                    {
58                        "pid": [ 3338 ],
59                        "cpu": [0, 2, 4],
60                    }
61    :type filters: dict
62
63    :param per_line: Used to control the number of graphs
64        in each graph subplot row
65    :type per_line: int
66
67    :param concat: Draw all the pivots on a single graph
68    :type concat: bool
69
70    :param permute: Draw one plot for each of the traces specified
71    :type permute: bool
72
73    :param drawstyle: This argument is forwarded to the matplotlib
74        corresponding :func:`matplotlib.pyplot.plot` call
75
76        drawing style.
77
78        .. note::
79
80            step plots are not currently supported for filled
81            graphs
82
83    :param xlim: A tuple representing the upper and lower xlimits
84    :type xlim: tuple
85
86    :param ylim: A tuple representing the upper and lower ylimits
87    :type ylim: tuple
88
89    :param title: A title describing all the generated plots
90    :type title: str
91
92    :param style: Created pre-styled graphs loaded from
93        :mod:`trappy.plotter.AttrConf.MPL_STYLE`
94    :type style: bool
95
96    :param signals: A string of the type event_name:column
97        to indicate the value that needs to be plotted
98
99        .. note::
100
101            - Only one of `signals` or both `templates` and
102              `columns` should be specified
103            - Signals format won't work for :mod:`pandas.DataFrame`
104              input
105
106    :type signals: str
107
108    :param legend_ncol: A positive integer that represents the
109        number of columns in the legend
110    :type legend_ncol: int
111    """
112    __metaclass__ = ABCMeta
113
114    def __init__(self, traces, templates, **kwargs):
115        self._fig = None
116        self._layout = None
117        super(StaticPlot, self).__init__(traces=traces,
118                                         templates=templates)
119
120        self.set_defaults()
121
122        for key in kwargs:
123            if key in AttrConf.ARGS_TO_FORWARD:
124                self._attr["args_to_forward"][key] = kwargs[key]
125            else:
126                self._attr[key] = kwargs[key]
127
128        if "signals" in self._attr:
129            self._describe_signals()
130
131        self._check_data()
132
133        if "column" not in self._attr:
134            raise RuntimeError("Value Column not specified")
135
136        zip_constraints = not self._attr["permute"]
137        self.c_mgr = ConstraintManager(traces, self._attr["column"],
138                                       self.templates, self._attr["pivot"],
139                                       self._attr["filters"],
140                                       zip_constraints=zip_constraints)
141
142    def savefig(self, *args, **kwargs):
143        """Save the plot as a PNG fill. This calls into
144        :mod:`matplotlib.figure.savefig`
145        """
146
147        if self._fig is None:
148            self.view()
149        self._fig.savefig(*args, **kwargs)
150
151    @abstractmethod
152    def set_defaults(self):
153        """Sets the default attrs"""
154        self._attr["width"] = AttrConf.WIDTH
155        self._attr["length"] = AttrConf.LENGTH
156        self._attr["per_line"] = AttrConf.PER_LINE
157        self._attr["concat"] = AttrConf.CONCAT
158        self._attr["filters"] = {}
159        self._attr["style"] = True
160        self._attr["permute"] = False
161        self._attr["pivot"] = AttrConf.PIVOT
162        self._attr["xlim"] = AttrConf.XLIM
163        self._attr["ylim"] = AttrConf.YLIM
164        self._attr["title"] = AttrConf.TITLE
165        self._attr["args_to_forward"] = {}
166        self._attr["map_label"] = {}
167        self._attr["_legend_handles"] = []
168        self._attr["_legend_labels"] = []
169        self._attr["legend_ncol"] = AttrConf.LEGEND_NCOL
170
171    def view(self, test=False):
172        """Displays the graph"""
173
174        if test:
175            self._attr["style"] = True
176            AttrConf.MPL_STYLE["interactive"] = False
177
178        permute = self._attr["permute"] and not self._attr["concat"]
179        if self._attr["style"]:
180            with plt.rc_context(AttrConf.MPL_STYLE):
181                self._resolve(permute, self._attr["concat"])
182        else:
183            self._resolve(permute, self._attr["concat"])
184
185    def make_title(self, constraint, pivot, permute, concat):
186        """Generates a title string for an axis"""
187        if concat:
188            return str(constraint)
189
190        if permute:
191            return constraint.get_data_name()
192        elif pivot != AttrConf.PIVOT_VAL:
193            return "{0}: {1}".format(self._attr["pivot"], self._attr["map_label"].get(pivot, pivot))
194        else:
195            return ""
196
197    def add_to_legend(self, series_index, handle, constraint, pivot, concat, permute):
198        """
199        Add series handles and names to the legend
200        A handle is returned from a plot on an axis
201        e.g. Line2D from axis.plot()
202        """
203        self._attr["_legend_handles"][series_index] = handle
204        legend_labels = self._attr["_legend_labels"]
205
206        if concat and pivot == AttrConf.PIVOT_VAL:
207            legend_labels[series_index] = self._attr["column"]
208        elif concat:
209            legend_labels[series_index] = "{0}: {1}".format(
210                self._attr["pivot"],
211                self._attr["map_label"].get(pivot, pivot)
212            )
213        elif permute:
214            legend_labels[series_index] = constraint._template.name + ":" + constraint.column
215        else:
216            legend_labels[series_index] = str(constraint)
217
218    def _resolve(self, permute, concat):
219        """Determine what data to plot on which axis"""
220        pivot_vals, len_pivots = self.c_mgr.generate_pivots(permute)
221        pivot_vals = list(pivot_vals)
222
223        num_of_axes = len(self.c_mgr) if concat else len_pivots
224
225        # Create a 2D Layout
226        self._layout = PlotLayout(
227            self._attr["per_line"],
228            num_of_axes,
229            width=self._attr["width"],
230            length=self._attr["length"],
231            title=self._attr['title'])
232
233        self._fig = self._layout.get_fig()
234
235        # Determine what constraint to plot and the corresponding pivot value
236        if permute:
237            legend_len = self.c_mgr._max_len
238            pivots = [y for _, y in pivot_vals]
239            c_dict = {c : str(c) for c in self.c_mgr}
240            c_list = sorted(c_dict.items(), key=lambda x: (x[1].split(":")[-1], x[1].split(":")[0]))
241            constraints = [c[0] for c in c_list]
242            cp_pairs = [(c, p) for c in constraints for p in sorted(set(pivots))]
243        else:
244            legend_len = len_pivots if concat else len(self.c_mgr)
245            pivots = pivot_vals
246            cp_pairs = [(c, p) for c in self.c_mgr for p in pivots if p in c.result]
247
248        # Initialise legend data and colormap
249        self._attr["_legend_handles"] = [None] * legend_len
250        self._attr["_legend_labels"] = [None] * legend_len
251
252        if "colors" in self._attr:
253            self._cmap = ColorMap.rgb_cmap(self._attr["colors"])
254        else:
255            self._cmap = ColorMap(legend_len)
256
257        # Group constraints/series with the axis they are to be plotted on
258        figure_data = ddict(list)
259        for i, (constraint, pivot) in enumerate(cp_pairs):
260            axis = self._layout.get_axis(constraint.trace_index if concat else i)
261            figure_data[axis].append((constraint, pivot))
262
263        # Plot each axis
264        for axis, series_list in figure_data.iteritems():
265            self.plot_axis(
266                axis,
267                series_list,
268                permute,
269                self._attr["concat"],
270                self._attr["args_to_forward"]
271            )
272            if self._attr["xlim"]:
273                axis.set_xlim(self._attr["xlim"])
274            if self._attr["ylim"]:
275                axis.set_ylim(self._attr["ylim"])
276
277        # Show legend
278        legend = self._fig.legend(self._attr["_legend_handles"],
279                         self._attr["_legend_labels"],
280                         loc='lower center',
281                         ncol=self._attr["legend_ncol"],
282                         borderaxespad=0.)
283        legend.get_frame().set_facecolor('#F4F4F4')
284
285        self._layout.finish(num_of_axes)
286
287    def plot_axis(self, axis, series_list, permute, concat, args_to_forward):
288        """Internal Method called to plot data (series_list) on a given axis"""
289        raise NotImplementedError("Method Not Implemented")
290