elektroyar
elektroyar личный блог
06 декабря 2019, 11:54

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

Однажды мне нужно было отрисовать пару графиков в консольной программе, написанной на С++. Можно было решить эту проблему двумя способами:
  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
30 Комментариев
  • Karim
    06 декабря 2019, 13:34
    И зачем такие сложности. Встроенных возможностей вполне хватает и без OpenGL.




    • Андрей К
      06 декабря 2019, 13:54
      Karim, подскажите, что за встроенные возможности?
      • Karim
        06 декабря 2019, 16:17
        Андрей К, Обычные функции для рисования.
        Например LineTo(Mem, x, y), ну и т.д.
        • Андрей К
          06 декабря 2019, 16:19
          Karim, ааа точно. В школе же проходил
        • Евгений Гуревич
          06 декабря 2019, 18:06
          Karim, вы сами на писали движок для рисования или где-то взяли? Если «где-то» — не поделитесь ссылочкой?
          • Karim
            06 декабря 2019, 18:19
            Евгений Гуревич, Да нет никакого движка. Берёте и рисуете. В любом учебнике по С++ написано, как рисовать.
  • Skifan
    06 декабря 2019, 14:00
    чет вы походу велосипед собираете 
  • day0markets.ru
    06 декабря 2019, 14:12
    есть же QT, там и графики и интерфейсики
  • Евгений Гуревич
    06 декабря 2019, 14:43
    А есть библиотеки, чтоб рисовать график в реальном времени, как в торговых терминалах, масштабировать по вертикали, горизонтали, и т.д.?
    • МХ
      06 декабря 2019, 22:33
      Евгений Гуревич, если под веб, то есть такое — www.tradingview.com/lightweight-charts/
  • Jame Bonds
    06 декабря 2019, 15:21
    Ни хрена себе, велосипед. Да тут целый танк получился.
  • 🗝Багатенький Буратина
    06 декабря 2019, 16:00
    OpenGL-я на машине может и не быть.
  • Gregori
    06 декабря 2019, 18:48

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

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

    • Unworldly
      07 декабря 2019, 03:56
      Как всё сложно у С++'ников.

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

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

      К сожалению, там жесть полная, а не профессионализм.
        • Unworldly
          08 декабря 2019, 05:23
          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
              08 декабря 2019, 22:19
              Unworldly, спасибо за ответ. С mutex-ами понял, я думал что их копии не будут создаваться, т.к. mutex объявлен как статичная переменная. Теперь учту что такое бывает. По хорошему лучше вообще все это обернуть в класс и сделать некоторые переменные и мьютексы приватными объектами. Но я решил что «и так сойдет».

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

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

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

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

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

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

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

              Именно так, кстати, действуют профессионалы.
      • Gregori
        07 декабря 2019, 15:15

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

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

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

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

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

         

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

  • Unworldly
    08 декабря 2019, 04:31

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


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

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


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

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


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

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


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

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

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

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

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

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

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн