Начало начал

В прошлый раз я остановился на построении таблицы значения функций. Пришла пора перейти к построению самого графика, ради чего все это, собственно, и начиналось.
Итак, основная идея состоит в следующем. Повернем координатную ось на 90 градусов по часовой стрелке. Это нужно для того, чтобы упростить построения, не храня данные о каждой точке в каком-нибудь листе.
Дальше ограничиваем координатную ось игрек 82 символами для лучшей читабельности графика. Понятно, что при этом мы теряем точность и график будет больше схематическим (слишком сжатым), особенно для «крутых» функций, но все же.
После этого мы высчитываем положение оси x относительно оси игрек, то есть ищем, в каком месте у нас будет точка (x, 0). Ну а потом построчно будем ставить в соответствие x значение функции y1 в этой точке.
Поехали
Для начала нам понадобится следующая формула:
Самому графику мы отведем 80 позиций, два оставшихся места будут чисто декоративными. Этой формулой мы находим, какой диапазон значений соответствует одной позиции в нашем графике. Тогда мы сможем правильно отметить на нем текущую точку.
Основные идеи обозначены, поэтому перейдем к самому коду
В том числе и благодаря комментариям к первой части, я узнал о такой штуке, как format. Мне она показалась действительно удобнее моих танцев с бубном, поэтому огромный кусок кода с вычисления отступов превратился в пару строк
Единичка отвечает за номер элемента, переданного в качестве аргумента функции format, то есть это «ссылка» (не в буквальном смысле) на переменную label, которую мы собственно выводим на экран. Нумерация идет точно так же, как и в листах — с нуля.
Пара символов :> используется для того, чтобы выровнять выводимый на экран текст по правой стороне. Ну а {0} после символа > нужен для того, чтобы определить то количество позиций строки, которое вам нужно.
То есть в данном случае мы резервируем для строки label len(label) + dial_length позиций, причем сам label занимает только len(label), и выравниваем внутри совокупности этих позиций текст по правой стороне.
эти строки эквивалентны
Да, для строк, наверное, проще использовать второй вариант, но применить первый для общего развития не помешает)
В format можно запихивать даже массивы типа r_value (в C++), то есть созданные непосредственно при передаче аргумента.
Зафиксируем переменные, которые у нас постоянны, то есть они не зависят от текущего значения функции.
В питоне нет условного const для обозначения констант, поэтому принято называть такие переменные заглавными буквами и просто их не изменять.
Так как RATIO по понятным причинам не может быть равно 0, MAX_Y1_VALUE_DIFFERENCE должно быть положительным числом. Именно для этого в правой части присваивания есть второе слагаемое.
Позицию оси икс мы высчитываем по формуле
Откуда берется эта формула? Мы просто высчитываем отношение отрезков (на оси игрек) от начала оси (min(y1)) до текущего значения функции (y1(x)) и отрезка от начала оси до ее конца (max(y1)). Ну и умножаем это отношение на 80, чтобы найти такое расстояние от начала оси до текущего значения в пробелах (поэтому можно использовать только целые числа), которое отразит отношение-формулу на графике.
Так как нас интересует позиция при y1(x) = 0, то подставляем в формулу необходимые значения и вуаля.
Теперь можно переходить непосредственно к печати значений
Цикл нам уже знаком. Придется второй раз посчитать каждое значение функции, чтобы не хранить их в листе или чём-то еще.
Позицию игрека вычисляем по выше приведенной формуле.
Верхнюю разность из формулы нам придется еще пофиксить, предусмотрев случай, когда в формуле будет получаться неопределенность вида ноль на ноль. Если такая неопределенность возникнет, то она будет означать, что текущее значение y1(x) = max(y1), а значит текущее значение игрека совпадет с концом оси y.
Эта часть кода непосредственно отвечает за печать самого графика
Здесь-таки 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), эти три случая и будем рассматривать.
Это если у нас есть отрицательные значения (y1(x) < 0). Если нет, то просто печатаем '|' и определяем позицию точки.
Ну и завершаем программу дорисовкой оси x.
Итак, код, который в итоге получился:
Запустим программу на нескольких тестах




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


В прошлый раз я остановился на построении таблицы значения функций. Пришла пора перейти к построению самого графика, ради чего все это, собственно, и начиналось.
Итак, основная идея состоит в следующем. Повернем координатную ось на 90 градусов по часовой стрелке. Это нужно для того, чтобы упростить построения, не храня данные о каждой точке в каком-нибудь листе.
Дальше ограничиваем координатную ось игрек 82 символами для лучшей читабельности графика. Понятно, что при этом мы теряем точность и график будет больше схематическим (слишком сжатым), особенно для «крутых» функций, но все же.
После этого мы высчитываем положение оси x относительно оси игрек, то есть ищем, в каком месте у нас будет точка (x, 0). Ну а потом построчно будем ставить в соответствие x значение функции y1 в этой точке.
Поехали
Для начала нам понадобится следующая формула:
Самому графику мы отведем 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 должно быть положительным числом. Именно для этого в правой части присваивания есть второе слагаемое.
Позицию оси икс мы высчитываем по формуле
Откуда берется эта формула? Мы просто высчитываем отношение отрезков (на оси игрек) от начала оси (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), эти три случая и будем рассматривать.
- y1(x) <= 0 — RATIO/2
В этом случае точка находится до оси x, поэтому мы ищем расстояние от точки до оси в пробелах. Из-за округления чисел может получится так, что значения переменных AXIS_X_POS и pos_of_y могут совпасть. Но такого быть не может, так как в этом случае мы бы попали в третий случай. У нас же точка не совпадает с осью х, поэтому необходимо дополнительное условие, которое будет уменьшать на 1 переменную pos_of_y в случае равенства. - y(x) >= 0 + RATIO/2
Случай идентичен первому случаю, только точка будет расположена с другой стороны от оси х и все вышеописанные действия под это корректируются - остальное
Самый простой случай — просто печатаем звездочку на месте оси
Это если у нас есть отрицательные значения (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')
Запустим программу на нескольких тестах




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