Блог им. elektroyar

Рисование графиков в С++

Однажды мне нужно было отрисовать пару графиков в консольной программе, написанной на С++. Можно было решить эту проблему двумя способами:
  1. Сохранить график в файле и нарисовать его в экселе или другой софтине, м.б. даже в онлайн рисовалке
  2. Рисовать график прямиком из программы
Первый способ мне не подходил, так как я проводил тестирование алгоритмов, и лишней возней с копированием данных заниматься не хотелось. Второй способ имеет множество решений, но увы я не нашел быстрого решения, чтобы библиотека для рисования не требовала целую кучу зависимостей. Обычно библиотеки для рисования из С++ программы хотят OpenCV или питон с матлабом. Еще как вариант я знаю SFML и ImGUI. Вопрос — нафига столько всего нужно для обычного графика, если по сути нужен OpenGL и все. Решил исправить эту проблему и набросал header-only С++ библиотеку, которая работает в отдельном потоке и способна рисовать графики зависимостей X от Y и тепловые карты. Из зависимостей библиотека требует FreeGLUT.

Как установить


Если использовать Code::Blocks и mingw, то нужно подключить библиотеку freeglut или freeglut_static (во втором случае нужно также установить макрос FREEGLUT_STATIC), а также opengl32winmmgdi32. Подключить в проекте заголовочный файл include/easy_plot.hpp, указать С++11 или выше.

Пример того, что может рисовать эта библиотека:


Рисование графиков в С++
Рисование графиков в С++

Рисование графиков в С++Рисование графиков в С++

Пример кода

#include "easy_plot.hpp"

int main(int argc, char* argv[]) {
    easy_plot::init(&argc, argv);
	
    std::vector<double> x = {0,1,0,1,2,0,1};
    easy_plot::plot("X", x);

    // ставим красный цвет линии
    std::vector<double> y = {0,2,3,4,2,0,1};
    easy_plot::plot("Y", y, easy_plot::LineSpec(1,0,0));

	
    std::vector<double> x2 = {0,2,6,7,8,10,12};
    easy_plot::plot("XY", x2, y, easy_plot::LineSpec(1,1,0));

    easy_plot::WindowSpec wstyle; // тут можно настроить стиль графика (цвет фона и пр.)
    // выводим три графика в одном
    easy_plot::plot<double>("Y1Y2Y3", wstyle, 3, x, easy_plot::LineSpec(1,0,0), x2, easy_plot::LineSpec(1,0,1), y,     easy_plot::LineSpec(0,1,0));

    while(true) {
        std::this_thread::yield();
    }
    return 0;
}
Рисование тепловой карты

#include "easy_plot.hpp"

int main(int argc, char* argv[]) {
    easy_plot::init(&argc, argv);
	
    easy_plot::WindowSpec image_wstyle;
    image_wstyle.is_grid = true;
    image_wstyle.height = 320;
    image_wstyle.width = 320;
    float image_data[32][32] = {};
    size_t image_ind = 0;
    for(size_t x = 0 ; x < 32; ++x) {
        for(size_t y = 0; y < 32; ++y, ++image_ind) {
            image_data[x][y] = 1024 - std::sqrt((x - 18) * (x - 18) + (y - 18) * (y - 18));
        }
    }

    image_wstyle.is_color_heatmap = true;
    easy_plot::draw_heatmap("image_heatmap", image_wstyle, &image_data[0][0], 32, 32);

    while(true) {
        std::this_thread::yield();
    }
    return 0;
}

Особенности библиотеки:

  • Функция plot может принимать такие параметры, как имя окна, стиль окна (различные настройки цвета и пр., см. WindowSpec), данные графиков и стиль линий.
  • Функция draw_heatmap может принимать такие параметры, как имя окна, стиль окна (различные настройки цвета и пр., см. WindowSpec), данные массива тепловой карты типа float и размер тепловой карты.
  • Если графиков несколько, изначально они будут расположены по всему экрану равномерно.
  • Если навести курсор мыши на график, можно узнать номер линии и данные по осям X и Y.
  • Рисование графиков и тепловой карты происходит в отдельном потоке.
  • При повторном вызове функции с уже существующим именем окна график будет перерисован. 
  • Можно сохранить график
Репозиторий: https://github.com/NewYaroslav/easy_plot_cpp
★19 | ₽ 27
:), а вы решили этот вопрос? веб сервер хороший вариант.
И зачем такие сложности. Встроенных возможностей вполне хватает и без OpenGL.




avatar

Karim

Karim, подскажите, что за встроенные возможности?
Андрей К, Обычные функции для рисования.
Например LineTo(Mem, x, y), ну и т.д.
Karim, ааа точно. В школе же проходил
Karim, вы сами на писали движок для рисования или где-то взяли? Если «где-то» — не поделитесь ссылочкой?
Евгений Гуревич, Да нет никакого движка. Берёте и рисуете. В любом учебнике по С++ написано, как рисовать.
чет вы походу велосипед собираете 
avatar

Skifan

есть же QT, там и графики и интерфейсики
avatar

day0markets

day0markets, а если без Qt? Што делать?
:), на машине веб-сокеты, на клиенте — UI, который работает с сокетами. Ну или лучше в вебе.
:), только на смартлабе ждешь статьи про программирование?
avatar

meat

А есть библиотеки, чтоб рисовать график в реальном времени, как в торговых терминалах, масштабировать по вертикали, горизонтали, и т.д.?
Евгений Гуревич, думаю что есть. Я намерен что-то подобное сделать, но как дополнение к ImGui
Евгений Гуревич, если под веб, то есть такое — www.tradingview.com/lightweight-charts/
avatar

МХ

Ни хрена себе, велосипед. Да тут целый танк получился.
avatar

Jame Bonds

OpenGL-я на машине может и не быть.

Как всё сложно у С++'ников. серьёзный подход. даже сказал бы профессиональный. На пайтон такие вещи попроще делаются.

последний раз графику использовал на c++ когда учился и писал в borland C. там это было примерно также просто как и на turbo pascal/qbasic. цикл по X, вычисление координат  из функции (y) с масштабированием, постановка точки через point (ну или можно и lineto, если точек недостаточно). 

avatar

Gregori

Как всё сложно у С++'ников.

Gregori, C++ только для профессионалов.

серьёзный подход. даже сказал бы профессиональный.

К сожалению, там жесть полная, а не профессионализм.
Unworldly, интересно узнать, в чем заключается жесть, т.к. я самоучка и опыта работе в команде программистов у меня почти нет. 
Unworldly, интересно узнать, в чем заключается жесть, т.к. я самоучка и опыта работе в команде программистов у меня почти нет.

elektroyar, первая жесть — это потенциально «размножающиеся» static-переменные в заголовочном файле, особенно mutex. Если я разобью свою программу на несколько модулей (файлов), каждый из которых включит заголовочный файл библиотеки, то у меня появится несколько mutex'ов, по одному на каждый модуль. Всё, защита от race condition сломана.

Добиться того, чтобы mutex все равно был только один, можно, вот пример с несколькими модулями, где показано, что из разных модулей в вашем коде получается разный адрес mutex'а, то есть, их там много.

А также дана правильная реализация, чтобы адрес был один на программу, то есть, чтобы mutex был единственным.

Вторая жесть — это то, что код не является exception-safe там, где это жизненно необходимо.

Вы защищаете mutex'ом изменения drawings. Перед изменением drawings соответствующий mutex lock'чится, после изменения — раз'lock'чивается назад. Как бы, всё хорошо. Но между этими двумя моментами вызываются функции push_back и make_shared, каждая из которых может выбросить исключение. Я, в коде своей программы, могу отловить исключения и продолжить работу дальше. Но, вот, только библиотечный код при этом не раз'lock'чит mutex, и при попытке повторно его за'lock'чить всё повиснет. Для избегания таких вещей в стандартной библиотеке специально имеется lock_guard, который раз'lock'чит mutex при любом развитии событий.

Третья жесть — вы вообще прогоняли свой же тест?
Он не падает где-то на 33-35 строке?

В функцию с переменным числом аргументов передаёте сами объекты, а в самой функции вычитываете их адреса. Но передали-то объекты, а не их адреса. Естественно, всё падает.

В функцию с переменным числом аргументов можно передавать только POD-типы, а вы аж вектор туда запихиваете. И LineSpec не является POD-типом. Чтобы он им стал, требуется оттуда выкинуть всё, что мешает скомпилировать его в pure C. Вам хочется использовать значения по умолчанию, но это можно сделать и с POD-типом, применив вспомогательную функцию. Вот набросок на эту тему. Значение последнего параметра берётся по умолчанию.

Вот здесь рекомендуют вместо функции с переменным числом аргументов использовать variadic templates или initializer_list. Но для этого, конечно, нужно владеть «шаблонной магией» хотя бы на среднем уровне.

Набросок решения с использованием variadic templates — здесь (общее количество пар заранее неизвестно).

Набросок решения с использованием initializer_list — здесь (общее количество пар в данном случае будет заранее известно).

В целом, остальное, навскидку, жестью не является, но очень много особенностей построения кода говорят о непонимании множества вещей.

Для написания библиотек, кстати, требуется более высокая квалификация, чем для написания обычных программ, потому что требуется учитывать множество вариантов, как пользователи библиотеки могут использовать её.
Unworldly, спасибо за ответ. С mutex-ами понял, я думал что их копии не будут создаваться, т.к. mutex объявлен как статичная переменная. Теперь учту что такое бывает. По хорошему лучше вообще все это обернуть в класс и сделать некоторые переменные и мьютексы приватными объектами. Но я решил что «и так сойдет».

Да, в данном случае можно применить lock_guard, просто по привычке его не ставил, чаще нет смысла блокировать мьютекс на всю область видимости.
В функцию с переменным числом аргументов передаёте сами объекты, а в самой функции вычитываете их адреса. Но передали-то объекты, а не их адреса. Естественно, всё падает.
Данная функция у меня лично нормально работала, совсем недавно ее использовал. Даже сейчас решил проверить, все отлично работает. 
В функцию с переменным числом аргументов передаёте сами объекты, а в самой функции вычитываете их адреса. Но передали-то объекты, а не их адреса. Естественно, всё падает.
Да, есть такой косяк. Но работает кстати в обоих случаях. Возможно дело в компиляторе mingw. Решил погуглить про особенности вариативных функций в С++. Вот что нашел: https://habr.com/ru/post/430064/ 
До C++11 не поддерживали аргументы не POD типов, а начиная с C++11 поддержка нетривиальных типов оставлена на усмотрение компилятора. Т.е. поведение кода зависит от компилятора и его версии.

Так или иначе, для лучшей совместимости, лучше переделать. 

Unworldly, спасибо за ответ. С mutex-ами понял, я думал что их копии не будут создаваться, т.к. mutex объявлен как статичная переменная. Теперь учту что такое бывает. По хорошему лучше вообще все это обернуть в класс и сделать некоторые переменные и мьютексы приватными объектами. Но я решил что «и так сойдет».

elektroyar, здесь важно понимать механизм явления.

Да, в данном случае можно применить lock_guard, просто по привычке его не ставил, чаще нет смысла блокировать мьютекс на всю область видимости.

Mutex следует блокировать на минимально возможное время, а область видимости можно задать самому с помощью искусственного добавленного блока.

Данная функция у меня лично нормально работала, совсем недавно ее использовал. Даже сейчас решил проверить, все отлично работает.

В той статье по вашей ссылке сказано, что  MSVC передаёт их по ссылке, что, в данном случае, эквивалентно указателю. Возможно, mingw в этой части «мимикрирует» под MSVC. Поэтому у вас и работает.

Если передать не вектор, а его адрес, а LineSpec сделать POD-типом, поправив соответствующую обработку в самой функции, то после этого компилируется тремя компиляторами и правильно работает под Linux'ом.

Так или иначе, для лучшей совместимости, лучше переделать.

Именно так, кстати, действуют профессионалы.

Unworldly, я бы не формудилировал так  «для профи»- «не для профи». На менее профессиональном 1С сделали в РФ разработчики денег поболее ))

а c++ вижу смысл использовать там где нужна высокая производительность и/или низкоуровневые вещи (разработка ОС..) 

И то с оговорками, например там где важна высокая надёжность(допустим embered системы самолётов и вооружения) пишутся зачастую на ada

Главное требование к языку- адекватности задачам.

++, кстати существенно java с c# подвинули за последнюю пару делителей.

 

Впрочем, поскольку человек пишет для себя- он вправе писать на том языке который лучше знает и который ему удобней и приятней использовать. Если ещё и на гитхаб выложил что бы с другими поделится- молодец ;-)

Unworldly, я бы не формудилировал так  «для профи»- «не для профи». На менее профессиональном 1С сделали в РФ разработчики денег поболее ))


Деньги здесь совершенно не при чём.

а c++ вижу смысл использовать там где нужна высокая производительность и/или низкоуровневые вещи (разработка ОС..)


Только при условии грамотного владения им.

++, кстати существенно java с c# подвинули за последнюю пару делителей.


Это как раз говорит о том, что C++ слишком сложен, вот люди и уходят на языки попроще.

Впрочем, поскольку человек пишет для себя- он вправе писать на том языке который лучше знает и который ему удобней и приятней использовать. Если ещё и на гитхаб выложил что бы с другими поделится- молодец ;-)


Вправе-то он, вправе, но это не отменяет той жести, которая, в результате, получается.

Чтобы овладеть C++, надо потратить многие годы, а чтобы потом поддерживать уровень, необходимо постоянно им заниматься, потому что каждые три года теперь выходит новый стандарт.

Разве любитель может выделить столько времени?
Поэтому я и говорю, что С++ — только для профессионалов.

avatar

Unworldly

Unworldly, в моем случае я долго занимаюсь С++, но в качестве своих проектов. Но проекты требуют чаще просто писать код, а не изучать непосредственно возможности языка. Поэтому последнее я стараюсь впихивать по возможности в новые задачи, чтобы параллельно что-то новое изучить. 
Unworldly, в моем случае я долго занимаюсь С++, но в качестве своих проектов. Но проекты требуют чаще просто писать код, а не изучать непосредственно возможности языка. Поэтому последнее я стараюсь впихивать по возможности в новые задачи, чтобы параллельно что-то новое изучить.

elektroyar, как показывает опыт, с C++ такой подход даёт плохие результаты.
Unworldly, если не секрет, вы совмещаете трейдинг с основной работой, где программируете на С++, или вы программируете в трейдинге на С++? 
Unworldly, если не секрет, вы совмещаете трейдинг с основной работой, где программируете на С++, или вы программируете в трейдинге на С++?

elektroyar, совмещаю трейдинг с основной работой, где программирую, в том числе, и на С++.

....все тэги
UPDONW