Wykres punktowy : klasa Plot i płynne API ========================================= W poprzednim ćwiczeniu powstała biblioteka klas opisujących elementy SVG: ``Shape``, ``Line``, ``Circle``, ``Rect``, ``Group``, ``Style`` oraz ``SVGDrawing``. Na końcu pokazano także przykład programu, który za pomocą tych klas rysuje prosty wykres funkcji sinus i cosinus. Celem tego ćwiczenia jest uporządkowanie tamtego przykładu i zamknięcie jego logiki w dwóch nowych klasach: - ``Axes`` — opisującej układ współrzędnych wykresu, - ``Plot`` — opisującej sam wykres punktowy. Dodatkowym celem ćwiczenia jest zastosowanie **płynnego API** (*fluent API*) podczas konfiguracji obiektu ``Axes``. Zadanie ------- Napisz moduł ``plot.py`` zawierający klasy ``Axes`` oraz ``Plot``. Nowe klasy mają korzystać z wcześniej przygotowanej biblioteki SVG. W szczególności klasa ``Axes`` ma być również obiektem rysowalnym, czyli powinna dziedziczyć po klasie ``Shape`` i implementować metodę ``draw()``. Klasa Axes ---------- Klasa ``Axes`` ma opisywać prostokątny obszar wykresu oraz sposób mapowania wartości numerycznych na współrzędne ekranu. Konstruktor klasy powinien przyjmować: - identyfikator obiektu, - współrzędne prostokąta rysowania: ``x0, y0, x1, y1``. Przykład: .. code-block:: python ax = Axes("ax1", 80, 40, 580, 260) Klasa ma udostępniać metody konfiguracyjne w stylu płynnego API, tzn. każda z nich powinna zwracać ``self``. Wymagane metody: - ``bottom(xmin, xmax)`` - ``top(xmin, xmax)`` - ``left(ymin, ymax)`` - ``right(ymin, ymax)`` - ``with_xticks(n)`` - ``with_yticks(n)`` - ``with_stroke(color)`` - ``with_stroke_width(width)`` Mile widziana (choć nie wymagana) jest też metoda ``with_grid()``, która umożliwi dodanie linii siatki do wykresu. Przykład użycia: .. code-block:: python axes = ( Axes("ax1", 80, 40, 580, 260) .bottom(0.0, 360) .left(-1.1, 1.1) .top(0.0, 360) .right(-1.1, 1.1) .with_xticks(6) .with_yticks(4) .with_grid(True) .with_stroke("black") .with_stroke_width(1.5) ) Klasa ``Axes`` powinna także udostępniać metody przeliczające współrzędne z układu danych do układu ekranu: - ``map_x(x)`` - ``map_y(y)`` Metoda ``draw()`` powinna zwracać fragment kodu SVG rysujący: - ramkę wykresu, - opcjonalną siatkę, jeśli została uwzględniona w implementacji. - znaczniki osi, jeśli zostały uwzględnione w implementacji. Nie trzeba rysować napisów, etykiet ani legendy. Klasa Plot ---------- Klasa ``Plot`` ma być prostym opakowaniem nad obiektem ``SVGDrawing``. Konstruktor: .. code-block:: python pl = Plot(axes) gdzie ``axes`` jest obiektem klasy ``Axes``. Klasa ``Plot`` powinna: - przechowywać obiekt ``Axes``, - utworzyć obiekt ``SVGDrawing``, - dodać do rysunku obiekt ``Axes``, - umożliwić nanoszenie punktów metodą ``scatter()``, - umożliwić zapis wyniku metodą ``save_fig()``. Wymagana metoda: .. code-block:: python scatter(x, y, marker="o", style=None) Metoda ``scatter()`` powinna: - przyjąć dwie listy liczb: ``x`` i ``y``, - sprawdzić, czy obie listy mają tę samą długość, - przeliczyć współrzędne punktów za pomocą metod ``map_x()`` i ``map_y()``, - utworzyć znaczniki odpowiedniego typu, - dodać je do rysunku jako grupę SVG. Zakładamy, że obsługa markerów została wydzielona do osobnego modułu (np. przez klasę ``MarkerFactory``), dlatego metoda ``scatter()`` powinna umieć korzystać z parametru ``marker``. Przykładowe znaczniki: - ``"o"`` — koło, - ``"d"`` — romb. Uwagi ----- 1. ``Axes`` ma dziedziczyć po ``Shape``. 2. Każda metoda płynnego API ma zwracać ``self``. 3. Obiekt ``Axes`` ma sam umieć się narysować. 4. Obiekt ``Plot`` ma korzystać z ``SVGDrawing``, a nie zastępować go. 5. Wykres ma być wykresem punktowym. 6. Styl punktów ma być przekazywany obiektem klasy ``Style``. 7. Wynik ma zostać zapisany do pliku SVG. Ćwiczenie to jest bezpośrednim rozwinięciem poprzedniego przykładu, w którym wykres był budowany „ręcznie” z obiektów ``Circle``, ``Line``, ``Group`` i ``SVGDrawing``. Teraz ta sama logika ma zostać zamknięta w klasach wyższego poziomu. Program testowy --------------- Końcowa implementacja musi poprawnie działać z poniższym kodem: .. code-block:: python import math from plot import Axes, Plot from svg_shapes import Style axes = ( Axes("ax1", 80, 40, 580, 260) .bottom(0.0, 360) .left(-1.1, 1.1) .top(0.0, 360) .right(-1.1, 1.1) .with_xticks(6) .with_yticks(4) .with_grid(True) .with_stroke("black") .with_stroke_width(1.5) ) x = [i for i in range(0, 370, 10)] ys = [math.sin(v*3.14159/180.0) for v in x] yc = [math.cos(v*3.14159/180.0) for v in x] pl = Plot(axes) sin_style = Style(fill="#a6cee3", stroke_width=1.5, stroke="#1f78b4") cos_style = Style(fill="#fdbf6f", stroke_width=1.5, stroke="#ff7f00") pl.scatter(x, ys, marker="o", style=sin_style) pl.scatter(x, yc, marker="d", style=cos_style) pl.save_fig("fluent_plot.svg") Wynik powinien przypominać wykres z poprzedniego ćwiczenia: ma zawierać ramkę, (opcjonalnie) siatkę oraz dwa zbiory punktów odpowiadające funkcjom sinus i cosinus, narysowane różnymi stylami.