diff options
author | Michael Schantl | 2023-09-06 12:53:25 +0000 |
---|---|---|
committer | Michael Schantl | 2023-09-06 12:53:25 +0000 |
commit | 47cf89105afb918583ae42af4eeb25658181a52a (patch) | |
tree | a53118deba32760704656320c70dd6316c4fbb30 | |
parent | d8172113415019e16ecf1450772bddc1041faf8d (diff) | |
download | aur-47cf89105afb918583ae42af4eeb25658181a52a.tar.gz |
upgpkg: weewx 4.10.2-6
Update Pillow to v10 by replacing genplot.py file with newer version.
-rw-r--r-- | .SRCINFO | 6 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | PKGBUILD | 10 | ||||
-rw-r--r-- | genplot.py | 737 |
4 files changed, 746 insertions, 9 deletions
@@ -1,7 +1,7 @@ pkgbase = weewx pkgdesc = Software for logging data from weather stations pkgver = 4.10.2 - pkgrel = 5 + pkgrel = 6 url = http://www.weewx.com/ arch = any license = GPL3 @@ -10,7 +10,7 @@ pkgbase = weewx depends = python-six depends = python-configobj depends = python-pyserial - depends = python-pillow95 + depends = python-pillow depends = python-pyusb depends = python-cheetah3 optdepends = python-pyephem: extended almanac information @@ -28,7 +28,7 @@ pkgbase = weewx source = weewxd source = wunderfixer source = weewx.service - source = pillow-rect.patch + source = genplot.py source = weewx-version.patch sha512sums = 8fca9cd7720a29687a0d900e4d89ec2ce5ca5d2aa36bc5b5909ea14ecb849cdbdb6e699cf1c3a0d5505c89ad8c309517db32fd8dc4a0ae4704dfd5ed0cc5747f sha512sums = SKIP diff --git a/.gitignore b/.gitignore index 8363df399f28..c40c2f1392b3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ !weewxd !wunderfixer !weewx-version.patch -!pillow-rect.patch +!genplot.py @@ -13,7 +13,7 @@ function _dl_url { echo "https://github.com/weewx/weewx/archive/refs/tags/v$1.$2.$3.tar.gz" } -pkgrel=5 +pkgrel=6 pkgdesc="Software for logging data from weather stations" arch=("any") url="http://www.weewx.com/" @@ -23,7 +23,7 @@ depends=("python" "python-six" "python-configobj" "python-pyserial" - "python-pillow95" + "python-pillow" "python-pyusb" "python-cheetah3") optdepends=("python-pyephem: extended almanac information" @@ -41,8 +41,8 @@ source=("$pkgname-$pkgver.tar.xz::$(_dl_url $_MAJOR $_MINOR $_PATCH)" "weewxd" "wunderfixer" "weewx.service" - "pillow-rect.patch" - "weewx-version.patch") + "genplot.py" + "weewx-version.patch") sha512sums=('8fca9cd7720a29687a0d900e4d89ec2ce5ca5d2aa36bc5b5909ea14ecb849cdbdb6e699cf1c3a0d5505c89ad8c309517db32fd8dc4a0ae4704dfd5ed0cc5747f' 'SKIP' 'SKIP' @@ -61,7 +61,7 @@ _watch="http://www.weewx.com/downloads/" prepare() { cd "$srcdir/${pkgname}-${pkgver}" patch --strip=1 --input="$srcdir/weewx-version.patch" - patch --strip=1 --input="$srcdir/pillow-rect.patch" + cp -v "$srcdir/genplot.py" bin/weeplot } build() { diff --git a/genplot.py b/genplot.py new file mode 100644 index 000000000000..7094d770e052 --- /dev/null +++ b/genplot.py @@ -0,0 +1,737 @@ +# +# Copyright (c) 2009-2023 Tom Keffer <tkeffer@gmail.com> +# +# See the file LICENSE.txt for your full rights. +# +"""Routines for generating image plots.""" + +import colorsys +import locale +import os +import time + +from PIL import Image, ImageDraw, ImageFont + +import weeplot.utilities +import weeutil.weeutil +from weeplot.utilities import tobgr +from weeutil.weeutil import max_with_none, min_with_none, to_bool + +# Test if this version of Pillow has ImageFont.getbbox. If not, we will activate a workaround. +try: + ImageFont.ImageFont.getbbox +except AttributeError: + PIL_HAS_BBOX = False +else: + PIL_HAS_BBOX = True + + +class GeneralPlot(object): + """Holds various parameters necessary for a plot. It should be specialized by the type of plot. + """ + def __init__(self, plot_dict): + """Initialize an instance of GeneralPlot. + + plot_dict: an instance of ConfigObj, or something that looks like it. + """ + + self.line_list = [] + + self.xscale = (None, None, None) + self.yscale = (None, None, None) + + self.anti_alias = int(plot_dict.get('anti_alias', 1)) + + self.image_width = int(plot_dict.get('image_width', 300)) * self.anti_alias + self.image_height = int(plot_dict.get('image_height', 180)) * self.anti_alias + self.image_background_color = tobgr(plot_dict.get('image_background_color', '0xf5f5f5')) + + self.chart_background_color = tobgr(plot_dict.get('chart_background_color', '0xd8d8d8')) + self.chart_gridline_color = tobgr(plot_dict.get('chart_gridline_color', '0xa0a0a0')) + color_list = plot_dict.get('chart_line_colors', ['0xff0000', '0x00ff00', '0x0000ff']) + fill_color_list = plot_dict.get('chart_fill_colors', color_list) + width_list = plot_dict.get('chart_line_width', [1, 1, 1]) + self.chart_line_colors = [tobgr(v) for v in color_list] + self.chart_fill_colors = [tobgr(v) for v in fill_color_list] + self.chart_line_widths = [int(v) for v in width_list] + + + self.top_label_font_path = plot_dict.get('top_label_font_path') + self.top_label_font_size = int(plot_dict.get('top_label_font_size', 10)) * self.anti_alias + + self.unit_label = None + self.unit_label_font_path = plot_dict.get('unit_label_font_path') + self.unit_label_font_color = tobgr(plot_dict.get('unit_label_font_color', '0x000000')) + self.unit_label_font_size = int(plot_dict.get('unit_label_font_size', 10)) * self.anti_alias + self.unit_label_position = (10 * self.anti_alias, 0) + + self.bottom_label = u"" + self.bottom_label_font_path = plot_dict.get('bottom_label_font_path') + self.bottom_label_font_color= tobgr(plot_dict.get('bottom_label_font_color', '0x000000')) + self.bottom_label_font_size = int(plot_dict.get('bottom_label_font_size', 10)) * self.anti_alias + self.bottom_label_offset = int(plot_dict.get('bottom_label_offset', 3)) + + self.axis_label_font_path = plot_dict.get('axis_label_font_path') + self.axis_label_font_color = tobgr(plot_dict.get('axis_label_font_color', '0x000000')) + self.axis_label_font_size = int(plot_dict.get('axis_label_font_size', 10)) * self.anti_alias + + # Make sure the formats used for the x- and y-axes are in unicode. + self.x_label_format = plot_dict.get('x_label_format') + self.y_label_format = plot_dict.get('y_label_format') + + self.x_nticks = int(plot_dict.get('x_nticks', 10)) + self.y_nticks = int(plot_dict.get('y_nticks', 10)) + + self.x_label_spacing = int(plot_dict.get('x_label_spacing', 2)) + self.y_label_spacing = int(plot_dict.get('y_label_spacing', 2)) + + # Calculate sensible margins for the given image and font sizes. + self.y_label_side = plot_dict.get('y_label_side', 'left') + if self.y_label_side == 'left' or self.y_label_side == 'both': + self.lmargin = int(4.0 * self.axis_label_font_size) + else: + self.lmargin = 20 * self.anti_alias + if self.y_label_side == 'right' or self.y_label_side == 'both': + self.rmargin = int(4.0 * self.axis_label_font_size) + else: + self.rmargin = 20 * self.anti_alias + self.bmargin = int(1.5 * (self.bottom_label_font_size + self.axis_label_font_size) + 0.5) + self.tmargin = int(1.5 * self.top_label_font_size + 0.5) + self.tbandht = int(1.2 * self.top_label_font_size + 0.5) + self.padding = 3 * self.anti_alias + + self.render_rose = False + self.rose_width = int(plot_dict.get('rose_width', 21)) + self.rose_height = int(plot_dict.get('rose_height', 21)) + self.rose_diameter = int(plot_dict.get('rose_diameter', 10)) + self.rose_position = (self.lmargin + self.padding + 5, self.image_height - self.bmargin - self.padding - self.rose_height) + self.rose_rotation = None + self.rose_label = plot_dict.get('rose_label', 'N') + self.rose_label_font_path = plot_dict.get('rose_label_font_path', self.bottom_label_font_path) + self.rose_label_font_size = int(plot_dict.get('rose_label_font_size', 10)) + self.rose_label_font_color = tobgr(plot_dict.get('rose_label_font_color', '0x000000')) + self.rose_line_width = int(plot_dict.get('rose_line_width', 1)) + self.rose_color = plot_dict.get('rose_color') + if self.rose_color is not None: + self.rose_color = tobgr(self.rose_color) + + # Show day/night transitions + self.show_daynight = to_bool(plot_dict.get('show_daynight', False)) + self.daynight_day_color = tobgr(plot_dict.get('daynight_day_color', '0xffffff')) + self.daynight_night_color = tobgr(plot_dict.get('daynight_night_color', '0xf0f0f0')) + self.daynight_edge_color = tobgr(plot_dict.get('daynight_edge_color', '0xefefef')) + self.daynight_gradient = int(plot_dict.get('daynight_gradient', 20)) + + # initialize the location + self.latitude = None + self.longitude = None + + # normalize the font paths relative to the skin directory + skin_dir = plot_dict.get('skin_dir', '') + self.top_label_font_path = self.normalize_path(skin_dir, self.top_label_font_path) + self.bottom_label_font_path = self.normalize_path(skin_dir, self.bottom_label_font_path) + self.unit_label_font_path = self.normalize_path(skin_dir, self.unit_label_font_path) + self.axis_label_font_path = self.normalize_path(skin_dir, self.axis_label_font_path) + self.rose_label_font_path = self.normalize_path(skin_dir, self.rose_label_font_path) + + @staticmethod + def normalize_path(skin_dir, path): + if path is None: + return None + return os.path.join(skin_dir, path) + + def setBottomLabel(self, bottom_label): + """Set the label to be put at the bottom of the plot. """ + self.bottom_label = bottom_label + + def setUnitLabel(self, unit_label): + """Set the label to be used to show the units of the plot. """ + self.unit_label = unit_label + + def setXScaling(self, xscale): + """Set the X scaling. + + xscale: A 3-way tuple (xmin, xmax, xinc) + """ + self.xscale = xscale + + def setYScaling(self, yscale): + """Set the Y scaling. + + yscale: A 3-way tuple (ymin, ymax, yinc) + """ + self.yscale = yscale + + def addLine(self, line): + """Add a line to be plotted. + + line: an instance of PlotLine + """ + if None in line.x: + raise weeplot.ViolatedPrecondition("X vector cannot have any values 'None' ") + self.line_list.append(line) + + def setLocation(self, lat, lon): + self.latitude = lat + self.longitude = lon + + def setDayNight(self, showdaynight, daycolor, nightcolor, edgecolor): + """Configure day/night bands. + + showdaynight: Boolean flag indicating whether to draw day/night bands + + daycolor: color for day bands + + nightcolor: color for night bands + + edgecolor: color for transition between day and night + """ + self.show_daynight = showdaynight + self.daynight_day_color = daycolor + self.daynight_night_color = nightcolor + self.daynight_edge_color = edgecolor + + def render(self): + """Traverses the universe of things that have to be plotted in this image, rendering + them and returning the results as a new Image object. + """ + + # NB: In what follows the variable 'draw' is an instance of an ImageDraw object and is in pixel units. + # The variable 'sdraw' is an instance of ScaledDraw and its units are in the "scaled" units of the plot + # (e.g., the horizontal scaling might be for seconds, the vertical for degrees Fahrenheit.) + image = Image.new("RGB", (self.image_width, self.image_height), self.image_background_color) + draw = ImageDraw.ImageDraw(image) + draw.rectangle(((self.lmargin,self.tmargin), + (self.image_width - self.rmargin, self.image_height - self.bmargin)), + fill=self.chart_background_color) + + self._renderBottom(draw) + self._renderTopBand(draw) + + self._calcXScaling() + self._calcYScaling() + self._calcXLabelFormat() + self._calcYLabelFormat() + + sdraw = self._getScaledDraw(draw) + if self.show_daynight: + self._renderDayNight(sdraw) + self._renderXAxes(sdraw) + self._renderYAxes(sdraw) + self._renderPlotLines(sdraw) + if self.render_rose: + self._renderRose(image, draw) + + if self.anti_alias != 1: + image.thumbnail((self.image_width / self.anti_alias, + self.image_height / self.anti_alias), + Image.LANCZOS) + + return image + + def _getScaledDraw(self, draw): + """Returns an instance of ScaledDraw, with the appropriate scaling. + + draw: An instance of ImageDraw + """ + sdraw = weeplot.utilities.ScaledDraw( + draw, + ( + (self.lmargin + self.padding, self.tmargin + self.padding), + (self.image_width - self.rmargin - self.padding, self.image_height - self.bmargin - self.padding) + ), + ( + (self.xscale[0], self.yscale[0]), + (self.xscale[1], self.yscale[1]) + ) + ) + return sdraw + + def _renderDayNight(self, sdraw): + """Draw vertical bands for day/night.""" + (first, transitions) = weeutil.weeutil.getDayNightTransitions( + self.xscale[0], self.xscale[1], self.latitude, self.longitude) + color = self.daynight_day_color \ + if first == 'day' else self.daynight_night_color + xleft = self.xscale[0] + for x in transitions: + sdraw.rectangle(((xleft,self.yscale[0]), + (x,self.yscale[1])), fill=color) + xleft = x + color = self.daynight_night_color \ + if color == self.daynight_day_color else self.daynight_day_color + sdraw.rectangle(((xleft,self.yscale[0]), + (self.xscale[1],self.yscale[1])), fill=color) + if self.daynight_gradient: + if first == 'day': + color1 = self.daynight_day_color + color2 = self.daynight_night_color + else: + color1 = self.daynight_night_color + color2 = self.daynight_day_color + nfade = self.daynight_gradient + # gradient is longer at the poles than the equator + d = 120 + 300 * (1 - (90.0 - abs(self.latitude)) / 90.0) + for i in range(len(transitions)): + last_ = self.xscale[0] if i == 0 else transitions[i-1] + next_ = transitions[i+1] if i < len(transitions)-1 else self.xscale[1] + for z in range(1,nfade): + c = blend_hls(color2, color1, float(z)/float(nfade)) + rgbc = int2rgbstr(c) + x1 = transitions[i]-d*(nfade+1)/2+d*z + if last_ < x1 < next_: + sdraw.rectangle(((x1, self.yscale[0]), + (x1+d, self.yscale[1])), + fill=rgbc) + if color1 == self.daynight_day_color: + color1 = self.daynight_night_color + color2 = self.daynight_day_color + else: + color1 = self.daynight_day_color + color2 = self.daynight_night_color + # draw a line at the actual sunrise/sunset + for x in transitions: + sdraw.line((x,x),(self.yscale[0],self.yscale[1]), + fill=self.daynight_edge_color) + + def _renderXAxes(self, sdraw): + """Draws the x axis and vertical constant-x lines, as well as the labels. """ + + axis_label_font = weeplot.utilities.get_font_handle(self.axis_label_font_path, + self.axis_label_font_size) + + drawlabelcount = 0 + for x in weeutil.weeutil.stampgen(self.xscale[0], self.xscale[1], self.xscale[2]) : + sdraw.line((x, x), + (self.yscale[0], self.yscale[1]), + fill=self.chart_gridline_color, + width=self.anti_alias) + if drawlabelcount % self.x_label_spacing == 0 : + xlabel = self._genXLabel(x) + if PIL_HAS_BBOX: + axis_label_width = sdraw.draw.textlength(xlabel, font=axis_label_font) + else: + axis_label_width, _ = sdraw.draw.textsize(xlabel, font=axis_label_font) + xpos = sdraw.xtranslate(x) + sdraw.draw.text((xpos - axis_label_width/2, self.image_height - self.bmargin + 2), + xlabel, fill=self.axis_label_font_color, font=axis_label_font) + drawlabelcount += 1 + + def _renderYAxes(self, sdraw): + """Draws the y axis and horizontal constant-y lines, as well as the labels. + Should be sufficient for most purposes. + """ + nygridlines = int((self.yscale[1] - self.yscale[0]) / self.yscale[2] + 1.5) + axis_label_font = weeplot.utilities.get_font_handle(self.axis_label_font_path, + self.axis_label_font_size) + + # Draw the (constant y) grid lines + for i in range(nygridlines) : + y = self.yscale[0] + i * self.yscale[2] + sdraw.line((self.xscale[0], self.xscale[1]), (y, y), fill=self.chart_gridline_color, + width=self.anti_alias) + # Draw a label on every other line: + if i % self.y_label_spacing == 0 : + ylabel = self._genYLabel(y) + if PIL_HAS_BBOX: + left, top, right, bottom = axis_label_font.getbbox(ylabel) + axis_label_width, axis_label_height = right - left, bottom - top + else: + axis_label_width, axis_label_height = sdraw.draw.textsize(ylabel, + font=axis_label_font) + ypos = sdraw.ytranslate(y) + # We want to treat Truetype and bitmapped fonts the same. By default, Truetype + # measures the top of the bounding box at the top of the ascender, while it's + # the top of the text for bitmapped. Specify an anchor of "lt" (left, top) for + # both. NB: argument "anchor" has been around at least as early as + # Pillow V5.0 (2018), but was not implemented until V8.0.0. + if self.y_label_side == 'left' or self.y_label_side == 'both': + sdraw.draw.text((self.lmargin - axis_label_width - 2, ypos - axis_label_height/2), + ylabel, fill=self.axis_label_font_color, font=axis_label_font, + anchor="lt") + if self.y_label_side == 'right' or self.y_label_side == 'both': + sdraw.draw.text((self.image_width - self.rmargin + 4, ypos - axis_label_height/2), + ylabel, fill=self.axis_label_font_color, font=axis_label_font, + anchor="lt") + + def _renderPlotLines(self, sdraw): + """Draw the collection of lines, using a different color for each one. Because there is + a limited set of colors, they need to be recycled if there are very many lines. + """ + nlines = len(self.line_list) + ncolors = len(self.chart_line_colors) + nfcolors = len(self.chart_fill_colors) + nwidths = len(self.chart_line_widths) + + # Draw them in reverse order, so the first line comes out on top of the image + for j, this_line in enumerate(self.line_list[::-1]): + + iline=nlines-j-1 + color = self.chart_line_colors[iline%ncolors] if this_line.color is None else this_line.color + fill_color = self.chart_fill_colors[iline%nfcolors] if this_line.fill_color is None else this_line.fill_color + width = (self.chart_line_widths[iline%nwidths] if this_line.width is None else this_line.width) * self.anti_alias + + # Calculate the size of a gap in data + maxdx = None + if this_line.line_gap_fraction is not None: + maxdx = this_line.line_gap_fraction * (self.xscale[1] - self.xscale[0]) + + if this_line.plot_type == 'line': + ms = this_line.marker_size + if ms is not None: + ms *= self.anti_alias + sdraw.line(this_line.x, + this_line.y, + line_type=this_line.line_type, + marker_type=this_line.marker_type, + marker_size=ms, + fill = color, + width = width, + maxdx = maxdx) + elif this_line.plot_type == 'bar' : + for x, y, bar_width in zip(this_line.x, this_line.y, this_line.bar_width): + if y is None: + continue + sdraw.rectangle(((x - bar_width, self.yscale[0]), (x, y)), fill=fill_color, outline=color) + elif this_line.plot_type == 'vector' : + for (x, vec) in zip(this_line.x, this_line.y): + sdraw.vector(x, vec, + vector_rotate = this_line.vector_rotate, + fill = color, + width = width) + self.render_rose = True + self.rose_rotation = this_line.vector_rotate + if self.rose_color is None: + self.rose_color = color + + def _renderBottom(self, draw): + """Draw anything at the bottom (just some text right now). """ + bottom_label_font = weeplot.utilities.get_font_handle(self.bottom_label_font_path, + self.bottom_label_font_size) + if PIL_HAS_BBOX: + left, top, right, bottom = bottom_label_font.getbbox(self.bottom_label) + bottom_label_width, bottom_label_height = right - left, bottom - top + else: + bottom_label_width, bottom_label_height = draw.textsize(self.bottom_label, + font=bottom_label_font) + draw.text(((self.image_width - bottom_label_width)/2, + self.image_height - bottom_label_height - self.bottom_label_offset), + self.bottom_label, + fill=self.bottom_label_font_color, + font=bottom_label_font, + anchor="lt") + + def _renderTopBand(self, draw): + """Draw the top band and any text in it. """ + # Draw the top band rectangle + draw.rectangle(((0,0), + (self.image_width, self.tbandht)), + fill = self.chart_background_color) + + # Put the units in the upper left corner + unit_label_font = weeplot.utilities.get_font_handle(self.unit_label_font_path, + self.unit_label_font_size) + if self.unit_label: + if self.y_label_side == 'left' or self.y_label_side == 'both': + draw.text(self.unit_label_position, + self.unit_label, + fill=self.unit_label_font_color, + font=unit_label_font) + if self.y_label_side == 'right' or self.y_label_side == 'both': + unit_label_position_right = (self.image_width - self.rmargin + 4, 0) + draw.text(unit_label_position_right, + self.unit_label, + fill=self.unit_label_font_color, + font=unit_label_font) + + top_label_font = weeplot.utilities.get_font_handle(self.top_label_font_path, + self.top_label_font_size) + + # The top label is the appended label_list. However, it has to be drawn in segments + # because each label may be in a different color. For now, append them together to get + # the total width + top_label = u' '.join([line.label for line in self.line_list]) + if PIL_HAS_BBOX: + top_label_width= draw.textlength(top_label, font=top_label_font) + else: + top_label_width, _ = draw.textsize(top_label, font=top_label_font) + + x = (self.image_width - top_label_width)/2 + y = 0 + + ncolors = len(self.chart_line_colors) + for i, this_line in enumerate(self.line_list): + color = self.chart_line_colors[i%ncolors] if this_line.color is None else this_line.color + # Draw a label + draw.text( (x,y), this_line.label, fill = color, font = top_label_font) + # Now advance the width of the label we just drew, plus a space: + if PIL_HAS_BBOX: + label_width = draw.textlength(this_line.label + u' ', font= top_label_font) + else: + label_width, _ = draw.textsize(this_line.label + u' ', font= top_label_font) + x += label_width + + def _renderRose(self, image, draw): + """Draw a compass rose.""" + + rose_center_x = self.rose_width/2 + 1 + rose_center_y = self.rose_height/2 + 1 + barb_width = 3 + barb_height = 3 + # The background is all white with a zero alpha (totally transparent) + rose_image = Image.new("RGBA", (self.rose_width, self.rose_height), (0x00, 0x00, 0x00, 0x00)) + rose_draw = ImageDraw.Draw(rose_image) + + fill_color = add_alpha(self.rose_color) + # Draw the arrow straight up (North). First the shaft: + rose_draw.line( ((rose_center_x, 0), (rose_center_x, self.rose_height)), + width = self.rose_line_width, + fill = fill_color) + # Now the left barb: + rose_draw.line( ((rose_center_x - barb_width, barb_height), (rose_center_x, 0)), + width = self.rose_line_width, + fill = fill_color) + # And the right barb: + rose_draw.line( ((rose_center_x, 0), (rose_center_x + barb_width, barb_height)), + width = self.rose_line_width, + fill = fill_color) + + rose_draw.ellipse(((rose_center_x - self.rose_diameter/2, + rose_center_y - self.rose_diameter/2), + (rose_center_x + self.rose_diameter/2, + rose_center_y + self.rose_diameter/2)), + outline = fill_color) + + # Rotate if necessary: + if self.rose_rotation: + rose_image = rose_image.rotate(self.rose_rotation) + rose_draw = ImageDraw.Draw(rose_image) + + # Calculate the position of the "N" label: + rose_label_font = weeplot.utilities.get_font_handle(self.rose_label_font_path, + self.rose_label_font_size) + if PIL_HAS_BBOX: + left, top, right, bottom = rose_label_font.getbbox(self.rose_label) + rose_label_width, rose_label_height = right - left, bottom - top + else: + rose_label_width, rose_label_height = draw.textsize(self.rose_label, + font=rose_label_font) + + # Draw the label in the middle of the (possibly) rotated arrow + rose_draw.text((rose_center_x - rose_label_width/2 - 1, + rose_center_y - rose_label_height/2 - 1), + self.rose_label, + fill=add_alpha(self.rose_label_font_color), + font=rose_label_font, + anchor="lt") + + # Paste the image of the arrow on to the main plot. The alpha + # channel of the image will be used as the mask. + # This will cause the arrow to overlay the background plot + image.paste(rose_image, self.rose_position, rose_image) + + def _calcXScaling(self): + """Calculates the x scaling. It will probably be specialized by + plots where the x-axis represents time. + """ + (xmin, xmax) = self._calcXMinMax() + + self.xscale = weeplot.utilities.scale(xmin, xmax, self.xscale, nsteps=self.x_nticks) + + def _calcYScaling(self): + """Calculates y scaling. Can be used 'as-is' for most purposes.""" + # The filter is necessary because unfortunately the value 'None' is not + # excluded from min and max (i.e., min(None, x) is not necessarily x). + # The try block is necessary because min of an empty list throws a + # ValueError exception. + ymin = ymax = None + for line in self.line_list: + if line.plot_type == 'vector': + try: + # For progressive vector plots, we want the magnitude of the complex vector + yline_max = max(abs(c) for c in [v for v in line.y if v is not None]) + except ValueError: + yline_max = None + yline_min = - yline_max if yline_max is not None else None + else: + yline_min = min_with_none(line.y) + yline_max = max_with_none(line.y) + ymin = min_with_none([ymin, yline_min]) + ymax = max_with_none([ymax, yline_max]) + + if ymin is None and ymax is None : + # No valid data. Pick an arbitrary scaling + self.yscale=(0.0, 1.0, 0.2) + else: + self.yscale = weeplot.utilities.scale(ymin, ymax, self.yscale, nsteps=self.y_nticks) + + def _calcXLabelFormat(self): + if self.x_label_format is None: + self.x_label_format = weeplot.utilities.pickLabelFormat(self.xscale[2]) + + def _calcYLabelFormat(self): + if self.y_label_format is None: + self.y_label_format = weeplot.utilities.pickLabelFormat(self.yscale[2]) + + def _genXLabel(self, x): + xlabel = locale.format_string(self.x_label_format, x) + return xlabel + + def _genYLabel(self, y): + ylabel = locale.format_string(self.y_label_format, y) + return ylabel + + def _calcXMinMax(self): + xmin = xmax = None + for line in self.line_list: + xline_min = min_with_none(line.x) + xline_max = max_with_none(line.x) + # If the line represents a bar chart, then the actual minimum has to + # be adjusted for the bar width of the first point + if line.plot_type == 'bar': + xline_min = xline_min - line.bar_width[0] + xmin = min_with_none([xmin, xline_min]) + xmax = max_with_none([xmax, xline_max]) + return xmin, xmax + + +class TimePlot(GeneralPlot) : + """Class that specializes GeneralPlot for plots where the x-axis is time.""" + + def _calcXScaling(self): + """Specialized version for time plots.""" + if None in self.xscale: + (xmin, xmax) = self._calcXMinMax() + self.xscale = weeplot.utilities.scaletime(xmin, xmax) + + def _calcXLabelFormat(self): + """Specialized version for time plots. Assumes that time is in unix epoch time.""" + if self.x_label_format is None: + (xmin, xmax) = self._calcXMinMax() + if xmin is not None and xmax is not None: + delta = xmax - xmin + if delta > 30*24*3600: + self.x_label_format = u"%x" + elif delta > 24*3600: + self.x_label_format = u"%x %X" + else: + self.x_label_format = u"%X" + + def _genXLabel(self, x): + """Specialized version for time plots. Assumes that time is in unix epoch time.""" + if self.x_label_format is None: + return u'' + time_tuple = time.localtime(x) + # There are still some strftimes out there that don't support Unicode. + try: + xlabel = time.strftime(self.x_label_format, time_tuple) + except UnicodeEncodeError: + # Convert it to UTF8, then back again: + xlabel = time.strftime(self.x_label_format.encode('utf-8'), time_tuple).decode('utf-8') + return xlabel + + +class PlotLine(object): + """Represents a single line (or bar) in a plot. """ + def __init__(self, x, y, label='', color=None, fill_color=None, width=None, plot_type='line', + line_type='solid', marker_type=None, marker_size=10, + bar_width=None, vector_rotate = None, line_gap_fraction=None): + self.x = x + self.y = y + self.label = label + self.plot_type = plot_type + self.line_type = line_type + self.marker_type = marker_type + self.marker_size = marker_size + self.color = color + self.fill_color = fill_color + self.width = width + self.bar_width = bar_width + self.vector_rotate = vector_rotate + self.line_gap_fraction = line_gap_fraction + + +def blend_hls(c, bg, alpha): + """Fade from c to bg using alpha channel where 1 is solid and 0 is + transparent. This fades across the hue, saturation, and lightness.""" + return blend(c, bg, alpha, alpha, alpha) + + +def blend_ls(c, bg, alpha): + """Fade from c to bg where 1 is solid and 0 is transparent. + Change only the lightness and saturation, not hue.""" + return blend(c, bg, 1.0, alpha, alpha) + + +def blend(c, bg, alpha_h, alpha_l, alpha_s): + """Fade from c to bg in the hue, lightness, saturation colorspace. + Added hue directionality to choose shortest circular hue path e.g. + https://stackoverflow.com/questions/1416560/hsl-interpolation + Also, grey detection to minimize colour wheel travel. Interesting resource: + http://davidjohnstone.net/pages/lch-lab-colour-gradient-picker + """ + + r1,g1,b1 = int2rgb(c) + h1,l1,s1 = colorsys.rgb_to_hls(r1/255.0, g1/255.0, b1/255.0) + + r2,g2,b2 = int2rgb(bg) + h2,l2,s2 = colorsys.rgb_to_hls(r2/255.0, g2/255.0, b2/255.0) + + # Check if either of the values is grey (saturation 0), + # in which case don't needlessly reset hue to '0', reducing travel around colour wheel + if s1 == 0: h1 = h2 + if s2 == 0: h2 = h1 + + h_delta = h2 - h1 + + if abs(h_delta) > 0.5: + # If interpolating over more than half-circle (0.5 radians) take shorter, opposite direction... + h_range = 1.0 - abs(h_delta) + h_dir = +1.0 if h_delta < 0.0 else -1.0 + + # Calculte h based on line back from h2 as proportion of h_range and alpha + h = h2 - ( h_dir * h_range * alpha_h ) + + # Clamp h within 0.0 to 1.0 range + h = h + 1.0 if h < 0.0 else h + h = h - 1.0 if h > 1.0 else h + else: + # Interpolating over less than a half-circle, so use normal interpolation as before + h = alpha_h * h1 + (1 - alpha_h) * h2 + + l = alpha_l * l1 + (1 - alpha_l) * l2 + s = alpha_s * s1 + (1 - alpha_s) * s2 + + r,g,b = colorsys.hls_to_rgb(h, l, s) + + r = round(r * 255.0) + g = round(g * 255.0) + b = round(b * 255.0) + + t = rgb2int(int(r),int(g),int(b)) + + return int(t) + + +def int2rgb(x): + b = (x >> 16) & 0xff + g = (x >> 8) & 0xff + r = x & 0xff + return r,g,b + + +def int2rgbstr(x): + return '#%02x%02x%02x' % int2rgb(x) + + +def rgb2int(r,g,b): + return r + g*256 + b*256*256 + + +def add_alpha(i): + """Add an opaque alpha channel to an integer RGB value""" + r = i & 0xff + g = (i >> 8) & 0xff + b = (i >> 16) & 0xff + a = 0xff # Opaque alpha + return r,g,b,a |