Класс графика
Алгоритм нормирования абсцисс и ординат проще создать, чем кратко и понятно описать. Тем не менее попробуем дать ключ к тому, что происходит. Мы хотим, чтобы размеры графика отслеживали размеры окна, а числа, используемые для разметки осей, из любого разумного диапазона, как можно дольше оставались читабельными. Задача трудновыполнимая, если динамически не изменять шрифт. В данной реализации мы не будем подбирать, а используем только два фиксированных шрифта: для оцифровки осей и для вывода заголовка графика.
Обычно при построении графиков числа, используемые для оцифровки осей (мантиссы), укладываются в некоторый разумный диапазон и принадлежат множеству чисел, кратных по модулю 10, стандартным значениям шага мантиссы (2, 2.5, 5 и 10). Операцию выбора шага сетки, удовлетворяющую этим условиям, удобно выполнить в глобально определенной функции, не принадлежащей классу CGraph. Это дает возможность использовать функцию для нужд других алгоритмов и классов. Ниже приведена функция gScale, которая выполняет подбор шага сетки. Мы постепенно дадим содержимое всего файла Graph.срр, поэтому вы можете полностью убрать существующие коды заготовки. Начало файла имеет такой вид:
#include "StdAfx.h" #include "graph.h" //===== Доля окна, занимаемая графиком #define SCAT,F,_X 0. 6 #define SCALE_Y 0.6 //=== Внешняя функция нормировки мантисс шагов сетки void gScale (double span, doubles step) { //== Переменная span определяет диапазон изменения //== значаний одной из координат точек графика //== Вычисляем порядок числа, описывающего диапазон int power = int(floor(loglO(span))); //===== Множитель (zoom factor) double factor = pow(10, power); //===== Мантисса диапазона (теперь 1 < span < 10) span /= factor; //===== Выбираем стандартный шаг сетки if (span<1.99) step=.2; else if (span<2.49) step=.25; else if (span<4.99) step=.5; else if (span<10.) step= 1.; //===== Возвращаем реальный шаг сетки (step*10~power) step *= factor; }
Результатом работы функции gScale является значение мантиссы дискретного шага сетки, которая наносится на график и оцифровывает оду из осей. Самым сложным местом в алгоритме разметки осей является метод CGraph::Scale. Он по очереди работает для обеих осей и поэтому использует параметр с данными типа TData, описывающими конкретную ось. Особенностью алгоритма является реализация идеи, принадлежащей доценту СПбГТУ Александру Калимову и заключающейся в том, чтобы как можно дольше не переходить к экспоненциальной форме записи чисел. Обычно Калимов использует форму с фиксированной запятой в диапазоне 7 порядков изменения чисел (10~3 +104), и это дает максимально удобный для восприятия формат, повышая читабельность графика:
void CGraph::Scale (TDatai data) { //===== С пустой последовательностью не работаем if (m_Points.empty()) return; //===== Готовимся искать экстремумы data.Max = data.bX? m_Points [0].х: m_Points [0].у; data.Min = data.Max; //===== Поиск экстремумов for (UINT j=0; j<ra_Point5.size(); j++) { double d = data.bX? m_Points [ j].x : m_Points [ j] .y; if (d < data.Min) data.Min = d; if (d > data.Max) data.Max = d; } //===== Максимальная амплитуда двух экстремумов double ext = max(fabs(data.Min),fabs(data.Max)); //===== Искусственно увеличиваем порядок экстремума //===== на 3 единицы, так как мы хотим покрыть 7 порядков, //===== не переходя к экспоненцеальной форме чисел double power = ext > 0.? loglO(ext) +3.: 0.; data.Power = int(floor(power/7.)); //===== Если число не укладывается в этот диапазон if (data.Power!= 0) //===== то мы восстанавливаем значение порядка data.Power = int(floor(power)) – 3; //===== Реальный множитель data.Factor = pow(10,data.Power); //===== Диапазон изменения мантиссы double span = (data.Max – data.Min)/data.Factor; //===== Если он нулевой, if (span == 0.) span = 0.5; // то искусственно раздвигаем график // Подбираем стандартный шаг для координатной сетки gScale (span, data.Step); //===== Шаг с учетом искусственных преобразований data.dStep = data.Step * data.Factor; //== Начальная линия сетки должна быть кратна шагу //==== и быть меньше минимума data.dStart = data.dStep * int (floor(data.Min/data.dStep)); data.Start = data.dStart/data.Factor; //===== Вычисляем последнюю линию сетки for (data.End = data.Start; data.End < data.Min/data.Factor + span-le-10; data.End += data.Step) data.dEnd = data.End*data.Factor; }