#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
import wx
class DynGraph:
def __init__(self, data, realSettings, realKoeffs):
self.grid = Grid(data, realSettings, realKoeffs)
self.lines = Lines(data, self.grid.GetXSampleWidth())
def Draw(self, dc):
drawer = Drawer(dc, self.grid.GetSampleSettings())
self.grid.Draw(drawer)
self.lines.Draw(drawer)
class Drawer:
def __init__(self, dc, sampleSettings):
self.dc = dc
w, h = dc.GetSizeTuple()
self.rect = [20, 20, w - 40, h - 40]
self.kMtoP = self.rect[3] / (sampleSettings.ySampleMax - sampleSettings.ySampleMin)
self.kStoP = self.rect[2] / (sampleSettings.xSampleWidth)
self.ypix0 = self.rect[1] + self.kMtoP*sampleSettings.ySampleMax
self.dc.SetBackground(wx.Brush(wx.LIGHT_GREY))
self.dc.Clear()
self.dc.DrawRectangleRect(self.rect)
self.font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
def DrawStepLine(self, line, step, color):
xFin = self.rect[0] + self.rect[2]
self.dc.SetPen(wx.Pen(color, 2))
dots = [(xFin - self.kStoP*i, self.ypix0 - y*self.kMtoP) for i, y in enumerate(reversed(line))]#self.kStoP * i
self.dc.DrawLines(dots)
self.dc.SetPen(wx.NullPen)
class SampleSettings:
def __init__(self, ySampleMin, ySampleMax, xSampleWidth):
self.ySampleMin, self.ySampleMax, self.xSampleWidth = (
ySampleMin, ySampleMax, xSampleWidth)
class Grid:
def __init__(self, data, realSettings, realKoeffs):
self.realSettings = realSettings
self.realKoeffs = realKoeffs
self.xSampleWidth = int(realSettings.xRealWidth / realKoeffs.kStoT) # yes, exception possible
self.ySampleMin = int(realSettings.yRealMin / realKoeffs.kMtoR)
self.ySampleMax = int(realSettings.yRealMax / realKoeffs.kMtoR)
def GetXSampleWidth(self):
return self.xSampleWidth
def GetSampleSettings(self):
return SampleSettings(self.ySampleMin, self.ySampleMax, self.xSampleWidth)
def Draw(self, drawer):
pass
class Lines:
def __init__(self, data, xSampleWidth):
self.data = data
self.xSampleWidth = xSampleWidth
def Draw(self, drawer):
beg, end = [len(self.data.mass[0]) - self.xSampleWidth, self.data.index]
if beg < 0: beg = 0
if end > len(self.data.mass[0]): end = -1
for lineNcolor in zip(self.data.mass, [(0xFF, 0x33, 0x33), (0x33, 0xFF, 0x33), (0x33, 0x33, 0xFF)]):
mass = lineNcolor[0][beg:end]
drawer.DrawStepLine(mass, 3, lineNcolor[1])
class RealSettings:
def __init__(self, yRealMax = 0, yRealMin = 0, xRealWidth = 0, xRealStep = 0, yRealStep = 0):
self.yRealMax = yRealMax
self.yRealMin = yRealMin
self.xRealWidth = xRealWidth
self.xRealStep = xRealStep
self.yRealStep = yRealStep
class RealKoeffs:
def __init__(self, kStoT = 1.0, kMtoR = 1.0):
self.kStoT = kStoT
self.kMtoR = kMtoR
class RegData:
def __init__(self, mass = [], index = -1):
self.mass = mass
self.index = index
class GraphWindow(wx.Window):
def __init__(self, parent):
wx.Window.__init__(self, parent)
someList = [5, 8, 6, 9, 7, 4]*50
funcs = [lambda x: 50 + x*10, lambda x: 150 + x*5, lambda x: 250 + x*3]
self.regData = RegData([[f(x) for x in someList] for f in funcs])
realSettings = RealSettings(yRealMax = 300, xRealWidth = 400, xRealStep = 50, yRealStep = 50)
self.graph = DynGraph(self.regData, realSettings, RealKoeffs(2.0, 1.01))
self.InitBuffer()
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnSize(self, evt):
# When the window size changes we need a new buffer.
self.InitBuffer()
def OnPaint(self, evt):
dc = wx.BufferedPaintDC(self, self.buffer)
def InitBuffer(self):
w, h = self.GetClientSize()
self.buffer = wx.EmptyBitmap(w, h)
dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
self.DrawGraph(dc)
def GetData(self):
return self.data
def DrawGraph(self, dc):
self.graph.Draw(dc)
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, title="Plot", size=(480,480))
self.plot = GraphWindow(self)
def Main():
app = wx.PySimpleApp()
frm = TestFrame()
frm.Show()
app.MainLoop()
if __name__ == "__main__":
Main()
Эксперимент: динамический график
Решил ради искупления за бессмысленный флейм сделать очередной одиночный проект. На этот раз постараюсь его сделать более демократичным. Буду делать динамический график, вроде того, что имеется в Диспетчере задач Windows, но с дополнительными возможностями. Выбрал эту задачу по двум причинам: кто-то тут уже искал подобное и не раз, и мне оно тоже скоро может пригодиться. То есть имеются некоторые стимулы.
Вступление 2.
Давно уже меня интересует феномен "правильного" ООП, заключающийся в том, что программа почти не будет изменяться при изменении применяемых технологий. Хочу попытаться сделать такое. Для этого сделаю следующие версии программ: на wxPython, затем на C++ и wxWidgets и наконец на C++ и Win32 API. Структура сильно меняться не должна. Интересно также сделать в связке Python + Tkinter, но, вероятно, не получится, так как мало стимулов.
Буду стараться проводить по одной итерации в день, хотя ясно, что процесс будет затухающим. К завтрашнему дню постараюсь написать черновик ТЗ.
Черновик ТЗ с картинкой добавил как файл pdf в архиве (к сожалению максимальный добавляемый файл pdf - 19.5 Кб). Без картинки выкладываю тут:
-----
График должен выглядеть приблизительно как показано на рисунке 1.
Рисунок 1. см. вложение
Самые последние значения появляются в левом краю и уходят в правый. Для просмотра истории используется ползунок.
Входные данные
Компонент графика должен получать набор массивов данных (или массив наборов данных — выясняется при разработке). Для эффективности данные передаются в целочисленном виде. Кроме того, датчик получает масштаб перевода данных в реальные единицы.
Подписи шкалы времени рассчитываются на основе данных конечной точки графика, масштаба по времени, просматриваемой точки времени.
Масштабирование
Масштабирование должно задаваться как в диалоге (не входит в компонент графика), так и с помощью "горячих клавиш".
Так как график динамический, то автомасштабирование не нужно: график, меняющий свой масштаб при каждом обновлении данных неудобен для восприятия.
Масштабирование с помощью диалога
При настройке в диалоге должны указываться следующие переменные: минимальное и максимальное значение по оси Y, шаг сетки по оси Y, период времени, выводимый на экран, шаг сетки по оси времени.
Масштабирование с помощью "горячих" клавиш
В режиме "горячих" клавиш логика работы должна быть примерно следующая:
1. При нажатии клавиши "+" должен меняться масштаб по оси Y в N раз (N определяется в настройках или константа). При этом центр оси Y рассчитывается от точки, в которой находится курсор мыши.
2. При нажатии сочетания клавиш "Shift" + "+" увеличивается масштаб по оси X в N раз. Тут положение курсора на вид никак не влияет - крайняя правая точка графика сохраняет своё значение.
Аналогичное действия должны выполняться при использовании клавиши "-", но с эффектом уменьшения.
Смещение по оси времени
В режиме отображения новых данных линии графика смещаются влево при обновлении данных. То есть последняя точка массива данных будет отображена в правом краю графика.
В режиме паузы пользователь может прокрутить график назад, насколько позволяет длина массива данных. Прокрутка может выполняться с помощью ползунка (скроллбара), либо с помощью курсора мыши в режиме "рука". При смещении ползунком положение линий графика по оси Y не меняется, при смещении "рукой" - меняется, в соответствии с перемещением курсора.
Настройка цвета линий графика
Каждая линия графика должна иметь свой цвет, который задается пользователем (или рассчитывается во внешней программе).
Отображение легенд
В легенде должно отображаться имя параметра и текущее значение (значение по оси Y при крайнем правом X). Цвет легенды должен совпадать с цветом линии. При использовании маркеров на линии графика, легенда должна выводить фигуру маркера перед именем.
Легенды могут выводиться внизу графика или справа.
Отображение сетки
Сетка может либо выводиться либо не выводиться. Для первой версии графика не предъявляются требования по ее стилю и цвету.
Требование по независимости от библиотек GUI
В компоненте графика следует минимизировать зависимость от применяемых библиотек GUI. Например, можно все операции прорисовки GUI спрятать в одном классе.
Для проверки независимости от GUI создать версии, работающие с разными библиотеками GUI.
-----
В следующей итерации проведу анализ.
Диаграмма предметной области графика показана на рисунке.
Описание основных классов
1. Объект класса Drawer осуществляет текста, линий, мультилиний. Он преобразует логические координаты в физические. Кроме того, возможно он фильтрует мультилинии (чтобы не повторять операции при совпадении координат соседних точек). В каком-то смысле он является обёрткой стандартных device context, но заточен на конкретное применение и имеет меньше операций.
2. Объект класса Lines имеет доступ к некоторым массивам данных вне компонента графика, к границам диапазонов, к легендам. Каждый массив ассоциирован со своей легендой. Lines хранит цвета линий графика, возможно, ширины и орнаменты. Lines выводит линии с помощью объекта Drawer.
3. Объект класса Grid - сетка с осями. Прорисовывает сетку и оси (объекты Axis) с помощью команд, адресованных объекту Drawer. Содержит объекты Axis, предназначенные для расчета значений и расположений меток осей. При расчетах учитываются пределы по осям X и Y.
Шаг 3. Расчёт преобразования координат.
Примечание: сегодня отмечал день рождение одной родственницы, потому изложение может быть немного путанным.
Введём 3 системы координат:
1. Время * измеряемая величина. Это реальные величины, которые могут быть не целочисленными.
2. Отсчеты * отсчеты. То есть, приведенные к целочисленным значениям величины первого пункта.
3. Пикселы * пикселы. Система координат для вывода.
Первая система координат используется для меток осей (то есть объект Grid) и для пользователя. Вторая систем координат используется объектами Grid, Lines и Drawer. Третья используется объектом Drawer.
Объект класса Grid получает коэффициенты перевода реальных единиц в отсчётные (целочисленные), а также границы реальной системы координат. И высчитывает размеры отсчетной области координат по примерно следующей формуле:
xSampleWidth = xRealWidth / kStoT
ySampleMin = yRealMin / kMtoR
ySampleMax = yRealMax / kMtoR
где kStoT - коэффициент переводящий отсчеты в реальные единицы времени,
kMtoR - коэффициент переводящий отсчёты в реальные единицы измеряемой величины.
Деление может быть заменено на умножение путём замены коэффициентов на обратные.
Эти границы передаются объекту Drawer. Ширина оси времени xSampleWidth передается объекту Lines, что бы он мог рассчитать, сколько отсчетов вывести.
Отсчеты в пикселы будем переводить вручную, чтоб не зависеть от конкретной системы, которая производит то же самое.
Объект класса Drawer рассчитывает коэффициенты перевода отсчетов в пикселы и ординату начала координат.
kMtoP = rectHeight / ySampleMax - ySampleMin
kStoP = rectWidth / xSampleWidth
ypix0 = rectYUp + kMtoP*ySampleMax
где rectYUp ордината верхнего левого угла прямоугольника, в который выводятся линии графика.
Таким образом, пискельные координаты считаются по формулам:
yPix = ypix0 - ySample*kMtoP
xPix = xRight - xSample*kStoP
где xRight - абсцисса правого края прямоугольника вывода линий графика.
В следующем шаге надо будет выложить код с какой-нибудь статической картинкой. В коде надо будет применить все имеющиеся расчеты.
Код запускает окно, в котором выводятся 3 статичные линии графика. Эти линии "резиновые" - то есть изменяются при изменении размера окна. Код работает в соответствии с формулами, приведенными в прошлом шаге. Некоторые куски кода с wxWidgets пока взяты из разных примеров по принципу Ctrl+C, Ctrl+V.
Код:
В следующем шаге сделаю "обрезку" прямоугольника, в который выводятся линии, планирую сделать вывод меток осей.
Сделал простенькие метки осей. В теме приведу только код класса Drawer. Весь код модуля прикреплю к теме для порядка. В нем есть несколько неявных ошибок. Кроме того, добавлю скриншот.
У класса добавились методы обрезки прямоугольника и методы добавления текста меток осей.
Код:
class Drawer:
def __init__(self, dc, sampleSettings):
self.dc = dc
w, h = dc.GetSizeTuple()
self.rect = [self.rectLeft, self.rectUp, self.rectWidth,
self.rectHeight] = [40, 40, w - 80, h - 80]
self.kMtoP = self.rectHeight / (sampleSettings.ySampleMax - sampleSettings.ySampleMin)
self.kStoP = self.rectWidth / (sampleSettings.xSampleWidth)
self.ypix0 = self.rectUp + self.kMtoP*sampleSettings.ySampleMax
self.dc.SetBackground(wx.Brush(wx.LIGHT_GREY))
self.dc.Clear()
self.dc.DrawRectangleRect(self.rect)
self.font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
def SetClipping(self):
self.dc.SetClippingRect(self.rect)
def DestroyClipping(self):
self.dc.DestroyClippingRegion()
def DrawStepLine(self, line, color):
xFin = self.rectLeft + self.rectWidth
self.dc.SetPen(wx.Pen(color, 2))
dots = [(xFin - self.kStoP*i, self.ypix0 - y*self.kMtoP) for i, y in enumerate(reversed(line))]
self.dc.DrawLines(dots)
self.dc.SetPen(wx.NullPen)
def DrawXAxisText(self, text, x):
xFin = self.rectLeft + self.rectWidth
yForXAxis = self.rectUp + self.rectHeight + 5
tw, th = self.dc.GetTextExtent(text)
self.dc.DrawText(text, xFin - self.kStoP*x - tw // 2, yForXAxis)
def DrawYAxisText(self, text, y):
tw, th = self.dc.GetTextExtent(text)
xForXAxis = self.rectLeft - tw - 5
self.dc.DrawText(text, xForXAxis, self.ypix0 - self.kMtoP*y - (th // 2))
def __init__(self, dc, sampleSettings):
self.dc = dc
w, h = dc.GetSizeTuple()
self.rect = [self.rectLeft, self.rectUp, self.rectWidth,
self.rectHeight] = [40, 40, w - 80, h - 80]
self.kMtoP = self.rectHeight / (sampleSettings.ySampleMax - sampleSettings.ySampleMin)
self.kStoP = self.rectWidth / (sampleSettings.xSampleWidth)
self.ypix0 = self.rectUp + self.kMtoP*sampleSettings.ySampleMax
self.dc.SetBackground(wx.Brush(wx.LIGHT_GREY))
self.dc.Clear()
self.dc.DrawRectangleRect(self.rect)
self.font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
def SetClipping(self):
self.dc.SetClippingRect(self.rect)
def DestroyClipping(self):
self.dc.DestroyClippingRegion()
def DrawStepLine(self, line, color):
xFin = self.rectLeft + self.rectWidth
self.dc.SetPen(wx.Pen(color, 2))
dots = [(xFin - self.kStoP*i, self.ypix0 - y*self.kMtoP) for i, y in enumerate(reversed(line))]
self.dc.DrawLines(dots)
self.dc.SetPen(wx.NullPen)
def DrawXAxisText(self, text, x):
xFin = self.rectLeft + self.rectWidth
yForXAxis = self.rectUp + self.rectHeight + 5
tw, th = self.dc.GetTextExtent(text)
self.dc.DrawText(text, xFin - self.kStoP*x - tw // 2, yForXAxis)
def DrawYAxisText(self, text, y):
tw, th = self.dc.GetTextExtent(text)
xForXAxis = self.rectLeft - tw - 5
self.dc.DrawText(text, xForXAxis, self.ypix0 - self.kMtoP*y - (th // 2))
Сегодня и завтра нормально поработать не получится. Поэтому на всякий случай составлю список замечаний к коду:
1. Названия некоторых переменных неудачны. Особенно названия коэффициентов.
2. Подписи меток осей выполняются объектом класса Grid неправильно: счёт должен быть от нуля.
3. Много потенциально опасного деления.
4. В некоторых методах смешаны действия расчета коэффициентов, отступов, размеров и действия вывода графики, например в конструкторе Drawer.
5. После отделения методов расчёта следует сделать юнит-тесты для них, чтобы можно было редактировать код более смело.
6. При любом обновлении графика создается новый объект класса Drawer. На данном этапе рано оптимизировать, но надо помнить, что некоторые вычисления конструктора нужны не так часто.
7. В классах GraphWindow, TestFrame некоторый бардак.
Есть еще мелкие недостатки которые надо убрать, прежде чем двигаться дальше.
Поправил некоторые пункты из предыдущего сообщения, добавил сетку и изменение графика со временем (запустил 3 случайные линии графика).
Этот шаг промежуточный, потому код выкладывать не буду, только скриншот.
Следующими шагами должны быть: тестирование, добавление легенд, добавление управления графиком (пауза, перемотка, масштабирование), ещё тестирование. На этом часть на wxPython будет закончена. Перейду к C++, если не отвлекусь на реализацию с Tkinter.
Пока выкладываю маленький кусочек с юнит-тестом,так как на большее не было времени.
Тестируемый класс:
Код:
class Drawer:
def __init__(self, dc, sampleSettings):
self.dc = dc
w, h = dc.GetSizeTuple()
self.CalcDimensions(w, h, sampleSettings)
self.dc.SetBackground(wx.Brush(wx.LIGHT_GREY))
self.dc.Clear()
self.dc.DrawRectangleRect(self.rect)
self.font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
def CalcDimensions(self, width, height, sampleSettings):
self.rect = [self.rectLeft, self.rectUp, self.rectWidth,
self.rectHeight] = [40, 40, width - 80, height - 80]
try:
self.kyStoP = self.rectHeight / (sampleSettings.ySampleMax - sampleSettings.ySampleMin)
self.kxStoP = self.rectWidth / (sampleSettings.xSampleWidth)
except ZeroDivisionError:
self.kyStoP, self.kxStoP = 1.0, 1.0
self.yPix0 = self.rectUp + self.kyStoP*sampleSettings.ySampleMax
def SetClipping(self):
self.dc.SetClippingRect(self.rect)
def DestroyClipping(self):
self.dc.DestroyClippingRegion()
def DrawStepLine(self, line, color):
xFin = self.rectLeft + self.rectWidth
self.dc.SetPen(wx.Pen(color, 2))
dots = [(xFin - self.kxStoP*i, self.yPix0 - y*self.kyStoP) for i, y in enumerate(reversed(line))]
self.dc.DrawLines(dots)
self.dc.SetPen(wx.NullPen)
def DrawXAxisText(self, text, x):
xFin = self.rectLeft + self.rectWidth
yForXAxis = self.rectUp + self.rectHeight + 5
tw, th = self.dc.GetTextExtent(text)
self.dc.DrawText(text, xFin - self.kxStoP*x - tw // 2, yForXAxis)
def DrawXAxisNet(self, x):
self.dc.SetPen(wx.Pen(wx.GREEN, 1, wx.DOT))
xFin = self.rectLeft + self.rectWidth
xPix = xFin - self.kxStoP*x
self.dc.DrawLine(xPix, self.rectUp, xPix, self.rectUp + self.rectHeight)
self.dc.SetPen(wx.NullPen)
def DrawYAxisNet(self, y):
self.dc.SetPen(wx.Pen(wx.GREEN, 1, wx.DOT))
yReal = self.yPix0 - y * self.kyStoP
self.dc.DrawLine(self.rectLeft, yReal, self.rectLeft + self.rectWidth, yReal)
self.dc.SetPen(wx.NullPen)
def DrawYAxisText(self, text, y):
tw, th = self.dc.GetTextExtent(text)
xForXAxis = self.rectLeft - tw - 5
self.dc.DrawText(text, xForXAxis, self.yPix0 - self.kyStoP*y - (th // 2))
def __init__(self, dc, sampleSettings):
self.dc = dc
w, h = dc.GetSizeTuple()
self.CalcDimensions(w, h, sampleSettings)
self.dc.SetBackground(wx.Brush(wx.LIGHT_GREY))
self.dc.Clear()
self.dc.DrawRectangleRect(self.rect)
self.font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
def CalcDimensions(self, width, height, sampleSettings):
self.rect = [self.rectLeft, self.rectUp, self.rectWidth,
self.rectHeight] = [40, 40, width - 80, height - 80]
try:
self.kyStoP = self.rectHeight / (sampleSettings.ySampleMax - sampleSettings.ySampleMin)
self.kxStoP = self.rectWidth / (sampleSettings.xSampleWidth)
except ZeroDivisionError:
self.kyStoP, self.kxStoP = 1.0, 1.0
self.yPix0 = self.rectUp + self.kyStoP*sampleSettings.ySampleMax
def SetClipping(self):
self.dc.SetClippingRect(self.rect)
def DestroyClipping(self):
self.dc.DestroyClippingRegion()
def DrawStepLine(self, line, color):
xFin = self.rectLeft + self.rectWidth
self.dc.SetPen(wx.Pen(color, 2))
dots = [(xFin - self.kxStoP*i, self.yPix0 - y*self.kyStoP) for i, y in enumerate(reversed(line))]
self.dc.DrawLines(dots)
self.dc.SetPen(wx.NullPen)
def DrawXAxisText(self, text, x):
xFin = self.rectLeft + self.rectWidth
yForXAxis = self.rectUp + self.rectHeight + 5
tw, th = self.dc.GetTextExtent(text)
self.dc.DrawText(text, xFin - self.kxStoP*x - tw // 2, yForXAxis)
def DrawXAxisNet(self, x):
self.dc.SetPen(wx.Pen(wx.GREEN, 1, wx.DOT))
xFin = self.rectLeft + self.rectWidth
xPix = xFin - self.kxStoP*x
self.dc.DrawLine(xPix, self.rectUp, xPix, self.rectUp + self.rectHeight)
self.dc.SetPen(wx.NullPen)
def DrawYAxisNet(self, y):
self.dc.SetPen(wx.Pen(wx.GREEN, 1, wx.DOT))
yReal = self.yPix0 - y * self.kyStoP
self.dc.DrawLine(self.rectLeft, yReal, self.rectLeft + self.rectWidth, yReal)
self.dc.SetPen(wx.NullPen)
def DrawYAxisText(self, text, y):
tw, th = self.dc.GetTextExtent(text)
xForXAxis = self.rectLeft - tw - 5
self.dc.DrawText(text, xForXAxis, self.yPix0 - self.kyStoP*y - (th // 2))
Модуль с тестом:
Код:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
from DynPlot import*
class EmptyDrawer(Drawer): # blemish detected!
def __init__(self): pass
class TestDrawer(unittest.TestCase):
def testDrawerCalcDimensions(self):
sc = EmptyDrawer()
sampleSettings = SampleSettings(-10, 50, 50)
sc.CalcDimensions(200, 200, sampleSettings)
#print sc.kxStoP, sc.kyStoP, sc.yPix0
self.assertAlmostEqual(sc.kxStoP, 2.4, 5)
self.assertAlmostEqual(sc.kyStoP, 2.0, 5)
self.assertAlmostEqual(sc.yPix0, 140, 3)
def testDrawerCalcDimensions2(self):
sc = EmptyDrawer()
sampleSettings = SampleSettings(11, 133, 89)
sc.CalcDimensions(407, 814, sampleSettings)
#print sc.kxStoP, sc.kyStoP, sc.yPix0
self.assertAlmostEqual(sc.kxStoP, 3.674157, 5)
self.assertAlmostEqual(sc.kyStoP, 6.016393, 5)
self.assertAlmostEqual(sc.yPix0, 840.18032, 3) # Why yPix0 is not int?
if __name__ == "__main__":
LoadCase = unittest.defaultTestLoader.loadTestsFromTestCase
suite = LoadCase(TestDrawer)
#suite.addTest(LoadCase(Test...))
unittest.TextTestRunner(verbosity = 2).run(suite)
# -*- coding: utf-8 -*-
import unittest
from DynPlot import*
class EmptyDrawer(Drawer): # blemish detected!
def __init__(self): pass
class TestDrawer(unittest.TestCase):
def testDrawerCalcDimensions(self):
sc = EmptyDrawer()
sampleSettings = SampleSettings(-10, 50, 50)
sc.CalcDimensions(200, 200, sampleSettings)
#print sc.kxStoP, sc.kyStoP, sc.yPix0
self.assertAlmostEqual(sc.kxStoP, 2.4, 5)
self.assertAlmostEqual(sc.kyStoP, 2.0, 5)
self.assertAlmostEqual(sc.yPix0, 140, 3)
def testDrawerCalcDimensions2(self):
sc = EmptyDrawer()
sampleSettings = SampleSettings(11, 133, 89)
sc.CalcDimensions(407, 814, sampleSettings)
#print sc.kxStoP, sc.kyStoP, sc.yPix0
self.assertAlmostEqual(sc.kxStoP, 3.674157, 5)
self.assertAlmostEqual(sc.kyStoP, 6.016393, 5)
self.assertAlmostEqual(sc.yPix0, 840.18032, 3) # Why yPix0 is not int?
if __name__ == "__main__":
LoadCase = unittest.defaultTestLoader.loadTestsFromTestCase
suite = LoadCase(TestDrawer)
#suite.addTest(LoadCase(Test...))
unittest.TextTestRunner(verbosity = 2).run(suite)
Тесты вскрыли то, что класс Drawer хорошо бы поделить на 2 класса. Это проявилось в том, что мне пришлось делать наследника класса, чтобы протестировать некоторую функцию.
Добавил легенды. Пока что под графиком. Надо сделать ещё возможность располагать их справа.
После добавления легенд код стал нечитаемым. Придется как-то менять структуру. Кроме того, для ускорения применил некоторые быдлокодерские приёмчики, которые надо устранять.
Юнит-тесты добавил. Но почти бессмысленные, для галочки, в надежде, что как-то помогут выявить дефекты при изменении структуры кода.
В общем, следующим ходом должно быть изменение структуры без добавления функциональности.
Для того, чтобы разобраться с бардаком, обратился к книге Мартина Фаулера про рефакторинг. Вооружившись некоторым знанием, начал менять код - добавил делегирование где мог, чтобы пользователь использовал только функции главного класса графика (DynGraph), некоторые данные сгруппировал классы, класс Drawer разбил на 2 части. Но пока всё в полуразборном виде, потому целиком не выкладываю, только некоторые части.
В классе Curve собрал данные для одной линии графика (цвет, имя, массив данных), в результате чего упростились классы Lines и Legends. Даже почти читаемые стали.
Код:
class Curve:
def __init__(self, mass, color = (0, 0, 0), name = None):
self.mass = mass
self.color = color
self.name = name
class Legends:
def __init__(self, curves):
self.curves = curves
def Draw(self, drawer):
for i, curve in enumerate(self.curves):
text = curve.name + ": " + str(curve.mass[-1])
drawer.DrawDownLegend(i, text, curve.color)
class Lines:
def __init__(self, data, xSampleWidth):
self.data = data
self.xSampleWidth = xSampleWidth
self.legends = None
self.curves = []
def LegendsVisible(self, flag = True):
if flag:
if not self.legends:
self.legends = Legends(self.curves)
else: self.legends = None
def SetCurves(self, curves):
self.curves = curves
def Draw(self, drawer):
drawer.SetClipping()
beg, end = self.data.index - self.xSampleWidth, self.data.index
if beg < 0: beg = 0
if end > len(self.curves[0].mass): end = -1
for curve in self.curves:
mass = curve.mass[beg:end]
drawer.DrawStepLine(mass, curve.color)
drawer.DestroyClipping()
if self.legends:
self.legends.Draw(drawer)
def GetLegendsOffset(self):
if self.legends:
return (len(self.curves), 0)
else:
return 0
def __init__(self, mass, color = (0, 0, 0), name = None):
self.mass = mass
self.color = color
self.name = name
class Legends:
def __init__(self, curves):
self.curves = curves
def Draw(self, drawer):
for i, curve in enumerate(self.curves):
text = curve.name + ": " + str(curve.mass[-1])
drawer.DrawDownLegend(i, text, curve.color)
class Lines:
def __init__(self, data, xSampleWidth):
self.data = data
self.xSampleWidth = xSampleWidth
self.legends = None
self.curves = []
def LegendsVisible(self, flag = True):
if flag:
if not self.legends:
self.legends = Legends(self.curves)
else: self.legends = None
def SetCurves(self, curves):
self.curves = curves
def Draw(self, drawer):
drawer.SetClipping()
beg, end = self.data.index - self.xSampleWidth, self.data.index
if beg < 0: beg = 0
if end > len(self.curves[0].mass): end = -1
for curve in self.curves:
mass = curve.mass[beg:end]
drawer.DrawStepLine(mass, curve.color)
drawer.DestroyClipping()
if self.legends:
self.legends.Draw(drawer)
def GetLegendsOffset(self):
if self.legends:
return (len(self.curves), 0)
else:
return 0
Некоторые дефекты остались - например, теперь требуется изменить класс RegData и изменить его имя.
Раскидал основные файлы по классам. И ещё некоторые детали добавил. Файл Main.pyw иллюстрирует применение, остальные - сам компонент графика.
Наверное, крайний раз тут выкладываю весь код. Надо уже переносить его в SVN.