Построение функций в консоли. Часть 2 (График) +3


Начало начал

image

В прошлый раз я остановился на построении таблицы значения функций. Пришла пора перейти к построению самого графика, ради чего все это, собственно, и начиналось.

Итак, основная идея состоит в следующем. Повернем координатную ось на 90 градусов по часовой стрелке. Это нужно для того, чтобы упростить построения, не храня данные о каждой точке в каком-нибудь листе.

Дальше ограничиваем координатную ось игрек 82 символами для лучшей читабельности графика. Понятно, что при этом мы теряем точность и график будет больше схематическим (слишком сжатым), особенно для «крутых» функций, но все же.

После этого мы высчитываем положение оси x относительно оси игрек, то есть ищем, в каком месте у нас будет точка (x, 0). Ну а потом построчно будем ставить в соответствие x значение функции y1 в этой точке.

Поехали

Для начала нам понадобится следующая формула:

$ratio = (max(y1) - min(y1)) / (80)$



Самому графику мы отведем 80 позиций, два оставшихся места будут чисто декоративными. Этой формулой мы находим, какой диапазон значений соответствует одной позиции в нашем графике. Тогда мы сможем правильно отметить на нем текущую точку.

Основные идеи обозначены, поэтому перейдем к самому коду

    dial_length = 12
    label = "График функции y1 = x**3 - 2*x**2 + 4*x - 8"
    print("{1:>{0}}".format(len(label) + dial_length, label), '\n')
    print("{aux[1]:>{aux[0]}}\n {aux[2]:>{aux[0]}}>\n".format(aux =
                                [dial_length + 82, 'y' , 82*'-']));

В том числе и благодаря комментариям к первой части, я узнал о такой штуке, как format. Мне она показалась действительно удобнее моих танцев с бубном, поэтому огромный кусок кода с вычисления отступов превратился в пару строк

print("{1:>{0}}".format(len(label) + dial_length, label), '\n')

Единичка отвечает за номер элемента, переданного в качестве аргумента функции format, то есть это «ссылка» (не в буквальном смысле) на переменную label, которую мы собственно выводим на экран. Нумерация идет точно так же, как и в листах — с нуля.

Пара символов :> используется для того, чтобы выровнять выводимый на экран текст по правой стороне. Ну а {0} после символа > нужен для того, чтобы определить то количество позиций строки, которое вам нужно.

То есть в данном случае мы резервируем для строки label len(label) + dial_length позиций, причем сам label занимает только len(label), и выравниваем внутри совокупности этих позиций текст по правой стороне.

print("{1:>{0}}".format(len(label) + dial_length, label), '\n')
print(dial_length*' ' + label, '\n') 

эти строки эквивалентны


Да, для строк, наверное, проще использовать второй вариант, но применить первый для общего развития не помешает)

print("{aux[1]:>{aux[0]}}\n {aux[2]:>{aux[0]}}>\n".format(aux =
                                [dial_length + 82, 'y' , 82*'-']));

В format можно запихивать даже массивы типа r_value (в C++), то есть созданные непосредственно при передаче аргумента.

Зафиксируем переменные, которые у нас постоянны, то есть они не зависят от текущего значения функции.

В питоне нет условного const для обозначения констант, поэтому принято называть такие переменные заглавными буквами и просто их не изменять.

MAX_Y1_VALUE_DIFFERENCE = (max_y1_value - min_y1_value) +                               (max_y1_value == min_y1_value)
RATIO = MAX_Y1_VALUE_DIFFERENCE / 80
AXIS_X_POS = abs(int((- min_y1_value) / RATIO))
if (AXIS_X_POS > 80):
    AXIS_X_POS = 81

Так как RATIO по понятным причинам не может быть равно 0, MAX_Y1_VALUE_DIFFERENCE должно быть положительным числом. Именно для этого в правой части присваивания есть второе слагаемое.

Позицию оси икс мы высчитываем по формуле

$80*(y1(x) - min(y1)) / (max(y1) - min(y1))$



Откуда берется эта формула? Мы просто высчитываем отношение отрезков (на оси игрек) от начала оси (min(y1)) до текущего значения функции (y1(x)) и отрезка от начала оси до ее конца (max(y1)). Ну и умножаем это отношение на 80, чтобы найти такое расстояние от начала оси до текущего значения в пробелах (поэтому можно использовать только целые числа), которое отразит отношение-формулу на графике.

Так как нас интересует позиция при y1(x) = 0, то подставляем в формулу необходимые значения и вуаля.

Теперь можно переходить непосредственно к печати значений

while (is_sequence_decreasing and from_x >= to_x) or             (not is_sequence_decreasing and from_x <= to_x):

        y1_cur_value = y1(from_x)
        cur_y1_value_and_min_difference = (y1_cur_value - min_y1_value) +                                           (y1_cur_value == min_y1_value) *                                           ((max_y1_value == min_y1_value))
        pos_of_y = int(cur_y1_value_and_min_difference * 80 /                        MAX_Y1_VALUE_DIFFERENCE)

Цикл нам уже знаком. Придется второй раз посчитать каждое значение функции, чтобы не хранить их в листе или чём-то еще.

Позицию игрека вычисляем по выше приведенной формуле.

Верхнюю разность из формулы нам придется еще пофиксить, предусмотрев случай, когда в формуле будет получаться неопределенность вида ноль на ноль. Если такая неопределенность возникнет, то она будет означать, что текущее значение y1(x) = max(y1), а значит текущее значение игрека совпадет с концом оси y.

       print("{1:^{0}.6g}".format(dial_length, from_x), end='')

        if (negative_value_exists):
            if y1_cur_value <= 0 - RATIO / 2:
                req_aux = AXIS_X_POS - pos_of_y
                if (req_aux != 0):
                    print(pos_of_y * ' ' + '*' + (req_aux - 1) * ' ' + '|')
                else:
                    print((AXIS_X_POS - 1) * ' ' + '*' + '|')
            elif y1_cur_value >= 0 + RATIO / 2:
                req_aux = pos_of_y - AXIS_X_POS
                if (req_aux != 0):
                    print(AXIS_X_POS * ' ' + '|' + (req_aux - 1) * ' ' + '*')
                else:
                    print((AXIS_X_POS) * ' ' + '|*')
            else:
                print(AXIS_X_POS * ' ' + '*')
        else:
            print('|' + pos_of_y* ' ' + '*')
            AXIS_X_POS = 0
        from_x += pace_x
    print((dial_length + AXIS_X_POS) * ' ' + '|\n',
          (dial_length + AXIS_X_POS - 3) * ' ' + 'x V')

Эта часть кода непосредственно отвечает за печать самого графика

print("{1:^{0}.6g}".format(dial_length, from_x), end='')

Здесь-таки format очень сильно пригодился и упростил код. ^ позволяет нам выровнять число по центру выделенной области (в данном случае, 12 позиций). g отвечает за числа — если у них нет дробной части, то печататься она и не будет (число как int), иначе — 6 знаков после запятой

Так как наш график ограничен пространством в 80 символов вдоль оси y, на нашем графике значение функции в точке будет совпадать с осью x не только в случае y1(x) = 0, но и в окрестности [0 — RATIO/2, 0 + RATIO/2].

Всего у нас есть три случая расположения звездочки (то есть точки) и вертикальной палки (то есть оси x): '*|' (y1(x) <= 0 — RATIO/2), '*' (0 — RATIO/2 < y1(x) < 0 + RATIO/2), '|*' (y1(x) >= 0 + RATIO/2), эти три случая и будем рассматривать.

  1. y1(x) <= 0 — RATIO/2
    В этом случае точка находится до оси x, поэтому мы ищем расстояние от точки до оси в пробелах. Из-за округления чисел может получится так, что значения переменных AXIS_X_POS и pos_of_y могут совпасть. Но такого быть не может, так как в этом случае мы бы попали в третий случай. У нас же точка не совпадает с осью х, поэтому необходимо дополнительное условие, которое будет уменьшать на 1 переменную pos_of_y в случае равенства.
  2. y(x) >= 0 + RATIO/2
    Случай идентичен первому случаю, только точка будет расположена с другой стороны от оси х и все вышеописанные действия под это корректируются
  3. остальное
    Самый простой случай — просто печатаем звездочку на месте оси

Это если у нас есть отрицательные значения (y1(x) < 0). Если нет, то просто печатаем '|' и определяем позицию точки.

Ну и завершаем программу дорисовкой оси x.

Итак, код, который в итоге получился:

dial_length = 12
    label = "График функции y1 = x**3 - 2*x**2 + 4*x - 8"
    print("{1:^{0}.6f}".format(dial_length, x_copy))

    print("{1:>{0}}".format(len(label) + dial_length, label), '\n')
    print("{aux[1]:>{aux[0]}}\n {aux[2]:>{aux[0]}}>\n".format(aux =
                                [dial_length + 81, 'y' , 82*'-']), end='');

    MAX_Y1_VALUE_DIFFERENCE = (max_y1_value - min_y1_value) +                               (max_y1_value == min_y1_value)
    RATIO = MAX_Y1_VALUE_DIFFERENCE / 80
    AXIS_X_POS = abs(int((- min_y1_value) / RATIO))
    if (AXIS_X_POS > 80):
        AXIS_X_POS = 81

    while (is_sequence_decreasing and from_x >= to_x) or             (not is_sequence_decreasing and from_x <= to_x):

        y1_cur_value = y1(from_x)
        cur_y1_value_and_min_difference = (y1_cur_value - min_y1_value) +                                           (y1_cur_value == min_y1_value) *                                           ((max_y1_value == min_y1_value))
        pos_of_y = int(cur_y1_value_and_min_difference * 80 /                        MAX_Y1_VALUE_DIFFERENCE)

        print("{1:^{0}.6g}".format(dial_length, from_x), end='')

        if (negative_value_exists):
            if y1_cur_value <= 0 - RATIO / 2:
                req_aux = AXIS_X_POS - pos_of_y
                if (req_aux != 0):
                    print(pos_of_y * ' ' + '*' + (req_aux - 1) * ' ' + '|')
                else:
                    print((AXIS_X_POS - 1) * ' ' + '*' + '|')
            elif y1_cur_value >= 0 + RATIO / 2:
                req_aux = pos_of_y - AXIS_X_POS
                if (req_aux != 0):
                    print(AXIS_X_POS * ' ' + '|' + (req_aux - 1) * ' ' + '*')
                else:
                    print((AXIS_X_POS) * ' ' + '|*')
            else:
                print(AXIS_X_POS * ' ' + '*')
        else:
            print('|' + pos_of_y* ' ' + '*')
            AXIS_X_POS = 0
        from_x += pace_x
    print((dial_length + AXIS_X_POS) * ' ' + '|\n',
          (dial_length + AXIS_X_POS - 3) * ' ' + 'x V')

Запустим программу на нескольких тестах

image
image
image
image

Оно работает)

image

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (2):

  1. Error1024
    /#19239493 / +2

    Мне кажется куча print-ов это нифига не быстро(а также глючно и сложно), намного быстрее будет сделать текстовой фреймбуфер из списка строк.

  2. pchelintsev_an
    /#19244223

    Подобную прогу когда-то на чистом C писал (под Linux с использованием функций библиотеки curses). Сырцы залил сюда. Отличие от того, что делал автор, — точки графика соединяются отрезками прямых, построенными в консоли по алгоритму Брезенхема. По скорости не знаю — может, у меня не самая эффективная реализация.