Plot project
This commit is contained in:
3
b/.gitignore
vendored
Normal file
3
b/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build/
|
||||||
|
examples/
|
||||||
|
backup.c
|
||||||
8
b/build.sh
Executable file
8
b/build.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ ! -e build ]; then
|
||||||
|
mkdir build
|
||||||
|
fi
|
||||||
|
cd build
|
||||||
|
clang ../src/main.c -g -Wextra -Werror -Wall -std=c11 -lSDL3 -lSDL3_ttf -lm -fdiagnostics-absolute-paths
|
||||||
|
./a.out
|
||||||
1
b/src/acwi.json
Normal file
1
b/src/acwi.json
Normal file
File diff suppressed because one or more lines are too long
1125
b/src/json.c
Executable file
1125
b/src/json.c
Executable file
File diff suppressed because it is too large
Load Diff
382
b/src/main.c
Normal file
382
b/src/main.c
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <float.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <time.h>
|
||||||
|
#define SDL_MAIN_USE_CALLBACKS
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_main.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
|
||||||
|
#define unused(x) ((void)x)
|
||||||
|
|
||||||
|
#include "plot.h"
|
||||||
|
#define JSON_LIBRARY_IMPL
|
||||||
|
#include "json.c"
|
||||||
|
#include "plot.c"
|
||||||
|
|
||||||
|
typedef struct AppState {
|
||||||
|
SDL_Window *window;
|
||||||
|
SDL_Renderer *renderer;
|
||||||
|
TTF_Font *font;
|
||||||
|
json_t *acwi;
|
||||||
|
PlotPoint *raw_prices;
|
||||||
|
PlotPoint *daily_returns;
|
||||||
|
PlotPoint *cumulative_returns;
|
||||||
|
PlotPoint *annualized_volatility;
|
||||||
|
int raw_price_count;
|
||||||
|
Plot plots[4];
|
||||||
|
int plot_count;
|
||||||
|
int active_plot;
|
||||||
|
} AppState;
|
||||||
|
|
||||||
|
static void dashboard_layout(SDL_FRect *rects, int rect_count, int window_width, int window_height) {
|
||||||
|
int cols = 2;
|
||||||
|
float gap = 12.0f;
|
||||||
|
float margin = 12.0f;
|
||||||
|
float cell_w = ((float)window_width - margin * 2.0f - gap) / 2.0f;
|
||||||
|
float cell_h = ((float)window_height - margin * 2.0f - gap) / 2.0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < rect_count; i += 1) {
|
||||||
|
int row = i / cols;
|
||||||
|
int col = i % cols;
|
||||||
|
rects[i] = (SDL_FRect) {
|
||||||
|
.x = margin + (float)col * (cell_w + gap),
|
||||||
|
.y = margin + (float)row * (cell_h + gap),
|
||||||
|
.w = cell_w,
|
||||||
|
.h = cell_h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is very inefficient: every call rasterizes text to a CPU surface,
|
||||||
|
// uploads it to a GPU texture, renders it, then destroys both objects.
|
||||||
|
// We only draw a small number of labels, so it should be fine for now.
|
||||||
|
static void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, float x, float y, float anchor_x, float anchor_y, SDL_Color color) {
|
||||||
|
SDL_Surface *surface = TTF_RenderText_Blended(font, text, strlen(text), color);
|
||||||
|
if (surface == NULL) {
|
||||||
|
SDL_Log("Could not render text '%s': %s", text, SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
|
||||||
|
if (texture == NULL) {
|
||||||
|
SDL_Log("Could not create text texture '%s': %s", text, SDL_GetError());
|
||||||
|
SDL_DestroySurface(surface);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_FRect dst = {
|
||||||
|
.x = x - (float)surface->w * anchor_x,
|
||||||
|
.y = y - (float)surface->h * anchor_y,
|
||||||
|
.w = (float)surface->w,
|
||||||
|
.h = (float)surface->h,
|
||||||
|
};
|
||||||
|
SDL_RenderTexture(renderer, texture, NULL, &dst);
|
||||||
|
|
||||||
|
SDL_DestroyTexture(texture);
|
||||||
|
SDL_DestroySurface(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct PlotBounds {
|
||||||
|
double min_x;
|
||||||
|
double max_x;
|
||||||
|
double min_y;
|
||||||
|
double max_y;
|
||||||
|
} PlotBounds;
|
||||||
|
|
||||||
|
static void bounds_include(PlotBounds *bounds, double x, double y) {
|
||||||
|
if (x < bounds->min_x) bounds->min_x = x;
|
||||||
|
if (x > bounds->max_x) bounds->max_x = x;
|
||||||
|
if (y < bounds->min_y) bounds->min_y = y;
|
||||||
|
if (y > bounds->max_y) bounds->max_y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_plot_point(PlotPoint *points, int *count, PlotBounds *bounds, double x, double y) {
|
||||||
|
points[*count] = (PlotPoint){.x = x, .y = y};
|
||||||
|
*count += 1;
|
||||||
|
bounds_include(bounds, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void free_plot_arrays(PlotPoint *raw_prices, PlotPoint *daily_returns, PlotPoint *cumulative_returns, PlotPoint *annualized_volatility, double *returns) {
|
||||||
|
free(raw_prices);
|
||||||
|
free(daily_returns);
|
||||||
|
free(cumulative_returns);
|
||||||
|
free(annualized_volatility);
|
||||||
|
free(returns);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
|
||||||
|
unused(argc);
|
||||||
|
unused(argv);
|
||||||
|
|
||||||
|
json_t *acwi = json_read("acwi.json");
|
||||||
|
if (acwi == NULL) {
|
||||||
|
SDL_Log("Could not read acwi.json");
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_t *timestamps = jq(acwi, "chart.result[0].timestamp");
|
||||||
|
json_t *closes = jq(acwi, "chart.result[0].indicators.quote[0].close");
|
||||||
|
if (timestamps == NULL || closes == NULL || timestamps->kind != JSON_ARRAY || closes->kind != JSON_ARRAY) {
|
||||||
|
SDL_Log("Could not find timestamp/close arrays in acwi.json");
|
||||||
|
json_free(acwi);
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = timestamps->array.len < closes->array.len ? timestamps->array.len : closes->array.len;
|
||||||
|
PlotPoint *raw_prices = calloc((size_t)len, sizeof(*raw_prices));
|
||||||
|
PlotPoint *daily_returns = calloc((size_t)len, sizeof(*daily_returns));
|
||||||
|
PlotPoint *cumulative_returns = calloc((size_t)len, sizeof(*cumulative_returns));
|
||||||
|
PlotPoint *annualized_volatility = calloc((size_t)len, sizeof(*annualized_volatility));
|
||||||
|
double *returns = calloc((size_t)len, sizeof(*returns));
|
||||||
|
if (raw_prices == NULL || daily_returns == NULL || cumulative_returns == NULL || annualized_volatility == NULL || returns == NULL) {
|
||||||
|
SDL_Log("Could not allocate plot data");
|
||||||
|
free_plot_arrays(raw_prices, daily_returns, cumulative_returns, annualized_volatility, returns);
|
||||||
|
json_free(acwi);
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlotBounds raw_bounds = {DBL_MAX, -DBL_MAX, DBL_MAX, -DBL_MAX};
|
||||||
|
PlotBounds daily_bounds = {DBL_MAX, -DBL_MAX, DBL_MAX, -DBL_MAX};
|
||||||
|
PlotBounds cumulative_bounds = {DBL_MAX, -DBL_MAX, DBL_MAX, -DBL_MAX};
|
||||||
|
PlotBounds volatility_bounds = {DBL_MAX, -DBL_MAX, DBL_MAX, -DBL_MAX};
|
||||||
|
int raw_count = 0;
|
||||||
|
int daily_count = 0;
|
||||||
|
int cumulative_count = 0;
|
||||||
|
int volatility_count = 0;
|
||||||
|
int return_count = 0;
|
||||||
|
bool have_previous_close = false;
|
||||||
|
double previous_close = 0.0;
|
||||||
|
double first_close = 0.0;
|
||||||
|
const int volatility_window = 30;
|
||||||
|
|
||||||
|
for (int i = 0; i < len; i += 1) {
|
||||||
|
json_t *x_node = timestamps->array.data[i];
|
||||||
|
json_t *close_node = closes->array.data[i];
|
||||||
|
if (x_node->kind != JSON_NUMBER || close_node->kind != JSON_NUMBER) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double x = x_node->number;
|
||||||
|
double close = close_node->number;
|
||||||
|
add_plot_point(raw_prices, &raw_count, &raw_bounds, x, close);
|
||||||
|
|
||||||
|
if (!have_previous_close) {
|
||||||
|
first_close = close;
|
||||||
|
add_plot_point(cumulative_returns, &cumulative_count, &cumulative_bounds, x, 0.0);
|
||||||
|
previous_close = close;
|
||||||
|
have_previous_close = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double daily_return = close / previous_close - 1.0;
|
||||||
|
double cumulative_return = close / first_close - 1.0;
|
||||||
|
returns[return_count] = daily_return;
|
||||||
|
return_count += 1;
|
||||||
|
|
||||||
|
add_plot_point(daily_returns, &daily_count, &daily_bounds, x, daily_return * 100.0);
|
||||||
|
add_plot_point(cumulative_returns, &cumulative_count, &cumulative_bounds, x, cumulative_return * 100.0);
|
||||||
|
|
||||||
|
if (return_count >= volatility_window) {
|
||||||
|
double sum = 0.0;
|
||||||
|
for (int j = return_count - volatility_window; j < return_count; j += 1) {
|
||||||
|
sum += returns[j];
|
||||||
|
}
|
||||||
|
double mean = sum / (double)volatility_window;
|
||||||
|
double variance = 0.0;
|
||||||
|
for (int j = return_count - volatility_window; j < return_count; j += 1) {
|
||||||
|
double d = returns[j] - mean;
|
||||||
|
variance += d * d;
|
||||||
|
}
|
||||||
|
variance /= (double)(volatility_window - 1);
|
||||||
|
double annualized = sqrt(variance) * sqrt(252.0) * 100.0;
|
||||||
|
add_plot_point(annualized_volatility, &volatility_count, &volatility_bounds, x, annualized);
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_close = close;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(returns);
|
||||||
|
returns = NULL;
|
||||||
|
|
||||||
|
if (raw_count < 2 || daily_count < 2 || cumulative_count < 2 || volatility_count < 2) {
|
||||||
|
SDL_Log("Not enough close data to build all plots");
|
||||||
|
free_plot_arrays(raw_prices, daily_returns, cumulative_returns, annualized_volatility, returns);
|
||||||
|
json_free(acwi);
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "wayland,x11,offscreen,dummy");
|
||||||
|
|
||||||
|
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||||
|
SDL_Log("Could not initialize SDL: %s", SDL_GetError());
|
||||||
|
free_plot_arrays(raw_prices, daily_returns, cumulative_returns, annualized_volatility, returns);
|
||||||
|
json_free(acwi);
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TTF_Init()) {
|
||||||
|
SDL_Log("Could not initialize SDL_ttf: %s", SDL_GetError());
|
||||||
|
free_plot_arrays(raw_prices, daily_returns, cumulative_returns, annualized_volatility, returns);
|
||||||
|
json_free(acwi);
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppState *state = calloc(1, sizeof(*state));
|
||||||
|
if (state == NULL) {
|
||||||
|
SDL_Log("Could not allocate app state");
|
||||||
|
TTF_Quit();
|
||||||
|
free_plot_arrays(raw_prices, daily_returns, cumulative_returns, annualized_volatility, returns);
|
||||||
|
json_free(acwi);
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->acwi = acwi;
|
||||||
|
state->raw_prices = raw_prices;
|
||||||
|
state->daily_returns = daily_returns;
|
||||||
|
state->cumulative_returns = cumulative_returns;
|
||||||
|
state->annualized_volatility = annualized_volatility;
|
||||||
|
state->raw_price_count = raw_count;
|
||||||
|
state->plot_count = 4;
|
||||||
|
state->active_plot = -1;
|
||||||
|
|
||||||
|
state->font = TTF_OpenFont("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12.0f);
|
||||||
|
if (state->font == NULL) {
|
||||||
|
SDL_Log("Could not open font: %s", SDL_GetError());
|
||||||
|
free_plot_arrays(state->raw_prices, state->daily_returns, state->cumulative_returns, state->annualized_volatility, returns);
|
||||||
|
json_free(state->acwi);
|
||||||
|
free(state);
|
||||||
|
TTF_Quit();
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->window = SDL_CreateWindow("ACWI Dashboard", 1200, 800, SDL_WINDOW_RESIZABLE);
|
||||||
|
if (state->window == NULL) {
|
||||||
|
SDL_Log("Could not create window: %s", SDL_GetError());
|
||||||
|
TTF_CloseFont(state->font);
|
||||||
|
free_plot_arrays(state->raw_prices, state->daily_returns, state->cumulative_returns, state->annualized_volatility, returns);
|
||||||
|
json_free(state->acwi);
|
||||||
|
free(state);
|
||||||
|
TTF_Quit();
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->renderer = SDL_CreateRenderer(state->window, NULL);
|
||||||
|
if (state->renderer == NULL) {
|
||||||
|
SDL_Log("Could not create renderer: %s", SDL_GetError());
|
||||||
|
SDL_DestroyWindow(state->window);
|
||||||
|
TTF_CloseFont(state->font);
|
||||||
|
free_plot_arrays(state->raw_prices, state->daily_returns, state->cumulative_returns, state->annualized_volatility, returns);
|
||||||
|
json_free(state->acwi);
|
||||||
|
free(state);
|
||||||
|
TTF_Quit();
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot_init(&state->plots[0], "ACWI close", "Date", "Close", raw_prices, raw_count, raw_bounds.min_x, raw_bounds.max_x, raw_bounds.min_y, raw_bounds.max_y, true, (SDL_Color){80, 210, 130, 255});
|
||||||
|
plot_init(&state->plots[1], "Daily return", "Date", "Return %", daily_returns, daily_count, daily_bounds.min_x, daily_bounds.max_x, daily_bounds.min_y, daily_bounds.max_y, true, (SDL_Color){90, 170, 255, 255});
|
||||||
|
plot_init(&state->plots[2], "Cumulative return", "Date", "Return %", cumulative_returns, cumulative_count, cumulative_bounds.min_x, cumulative_bounds.max_x, cumulative_bounds.min_y, cumulative_bounds.max_y, true, (SDL_Color){245, 180, 70, 255});
|
||||||
|
plot_init(&state->plots[3], "30-day annualized volatility", "Date", "Volatility %", annualized_volatility, volatility_count, volatility_bounds.min_x, volatility_bounds.max_x, volatility_bounds.min_y, volatility_bounds.max_y, true, (SDL_Color){220, 110, 220, 255});
|
||||||
|
|
||||||
|
printf("Loaded %d ACWI close-price points\n", state->raw_price_count);
|
||||||
|
*appstate = state;
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
|
||||||
|
AppState *state = appstate;
|
||||||
|
|
||||||
|
if (event->type == SDL_EVENT_QUIT) {
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
int window_width = 0;
|
||||||
|
int window_height = 0;
|
||||||
|
SDL_GetCurrentRenderOutputSize(state->renderer, &window_width, &window_height);
|
||||||
|
SDL_FRect rects[4];
|
||||||
|
dashboard_layout(rects, state->plot_count, window_width, window_height);
|
||||||
|
|
||||||
|
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN && event->button.button == SDL_BUTTON_LEFT) {
|
||||||
|
state->active_plot = -1;
|
||||||
|
for (int i = state->plot_count - 1; i >= 0; i -= 1) {
|
||||||
|
if (point_in_rect(event->button.x, event->button.y, rects[i])) {
|
||||||
|
state->active_plot = i;
|
||||||
|
state->plots[i].dragging = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_EVENT_MOUSE_BUTTON_UP && event->button.button == SDL_BUTTON_LEFT) {
|
||||||
|
if (state->active_plot >= 0) {
|
||||||
|
state->plots[state->active_plot].dragging = false;
|
||||||
|
}
|
||||||
|
state->active_plot = -1;
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_EVENT_MOUSE_MOTION && state->active_plot >= 0) {
|
||||||
|
Plot *plot = &state->plots[state->active_plot];
|
||||||
|
if (plot->dragging) {
|
||||||
|
plot_handle_mouse_motion(plot, rects[state->active_plot], event->motion.xrel);
|
||||||
|
}
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_EVENT_MOUSE_WHEEL && event->wheel.y != 0.0f) {
|
||||||
|
for (int i = state->plot_count - 1; i >= 0; i -= 1) {
|
||||||
|
SDL_FRect inner = plot_inner_rect(rects[i]);
|
||||||
|
if (point_in_rect(event->wheel.mouse_x, event->wheel.mouse_y, inner)) {
|
||||||
|
plot_handle_mouse_wheel(&state->plots[i], rects[i], event->wheel.mouse_x, event->wheel.y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppIterate(void *appstate) {
|
||||||
|
AppState *state = appstate;
|
||||||
|
|
||||||
|
int window_width = 0;
|
||||||
|
int window_height = 0;
|
||||||
|
if (!SDL_GetCurrentRenderOutputSize(state->renderer, &window_width, &window_height)) {
|
||||||
|
SDL_Log("Could not get render size: %s", SDL_GetError());
|
||||||
|
return SDL_APP_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(state->renderer, 10, 12, 18, 255);
|
||||||
|
SDL_RenderClear(state->renderer);
|
||||||
|
|
||||||
|
SDL_FRect rects[4];
|
||||||
|
dashboard_layout(rects, state->plot_count, window_width, window_height);
|
||||||
|
for (int i = 0; i < state->plot_count; i += 1) {
|
||||||
|
plot_render(&state->plots[i], state->renderer, state->font, rects[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_RenderPresent(state->renderer);
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL_AppQuit(void *appstate, SDL_AppResult result) {
|
||||||
|
unused(result);
|
||||||
|
|
||||||
|
AppState *state = appstate;
|
||||||
|
if (state != NULL) {
|
||||||
|
SDL_DestroyRenderer(state->renderer);
|
||||||
|
SDL_DestroyWindow(state->window);
|
||||||
|
TTF_CloseFont(state->font);
|
||||||
|
free_plot_arrays(state->raw_prices, state->daily_returns, state->cumulative_returns, state->annualized_volatility, NULL);
|
||||||
|
json_free(state->acwi);
|
||||||
|
free(state);
|
||||||
|
}
|
||||||
|
TTF_Quit();
|
||||||
|
}
|
||||||
196
b/src/plot.c
Normal file
196
b/src/plot.c
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
|
||||||
|
static bool point_in_rect(float x, float y, SDL_FRect rect) {
|
||||||
|
return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_FRect plot_inner_rect(SDL_FRect outer) {
|
||||||
|
SDL_FRect result = {
|
||||||
|
.x = outer.x + 58.0f,
|
||||||
|
.y = outer.y + 30.0f,
|
||||||
|
.w = outer.w - 76.0f,
|
||||||
|
.h = outer.h - 76.0f,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void plot_clamp_view_to_data(Plot *plot) {
|
||||||
|
double full_range = plot->max_x - plot->min_x;
|
||||||
|
double view_range = plot->view_max_x - plot->view_min_x;
|
||||||
|
double min_view_range = full_range * 0.001;
|
||||||
|
|
||||||
|
if (min_view_range <= 0.0) {
|
||||||
|
min_view_range = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view_range < min_view_range) {
|
||||||
|
double center = (plot->view_min_x + plot->view_max_x) * 0.5;
|
||||||
|
view_range = min_view_range;
|
||||||
|
plot->view_min_x = center - view_range * 0.5;
|
||||||
|
plot->view_max_x = center + view_range * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view_range >= full_range) {
|
||||||
|
plot->view_min_x = plot->min_x;
|
||||||
|
plot->view_max_x = plot->max_x;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plot->view_min_x < plot->min_x) {
|
||||||
|
plot->view_min_x = plot->min_x;
|
||||||
|
plot->view_max_x = plot->view_min_x + view_range;
|
||||||
|
}
|
||||||
|
if (plot->view_max_x > plot->max_x) {
|
||||||
|
plot->view_max_x = plot->max_x;
|
||||||
|
plot->view_min_x = plot->view_max_x - view_range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void plot_init(Plot *plot, const char *title, const char *x_axis_label, const char *y_axis_label, PlotPoint *points, int point_count, double min_x, double max_x, double min_y, double max_y, bool x_axis_is_time, SDL_Color line_color) {
|
||||||
|
snprintf(plot->title, sizeof(plot->title), "%s", title);
|
||||||
|
snprintf(plot->x_axis_label, sizeof(plot->x_axis_label), "%s", x_axis_label);
|
||||||
|
snprintf(plot->y_axis_label, sizeof(plot->y_axis_label), "%s", y_axis_label);
|
||||||
|
plot->points = points;
|
||||||
|
plot->point_count = point_count;
|
||||||
|
plot->min_x = min_x;
|
||||||
|
plot->max_x = max_x;
|
||||||
|
plot->min_y = min_y;
|
||||||
|
plot->max_y = max_y;
|
||||||
|
plot->view_min_x = min_x;
|
||||||
|
plot->view_max_x = max_x;
|
||||||
|
plot->x_axis_is_time = x_axis_is_time;
|
||||||
|
plot->dragging = false;
|
||||||
|
plot->line_color = line_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void plot_handle_mouse_motion(Plot *plot, SDL_FRect outer, float xrel) {
|
||||||
|
SDL_FRect inner = plot_inner_rect(outer);
|
||||||
|
double view_range = plot->view_max_x - plot->view_min_x;
|
||||||
|
double x_delta = -((double)xrel / (double)inner.w) * view_range;
|
||||||
|
plot->view_min_x += x_delta;
|
||||||
|
plot->view_max_x += x_delta;
|
||||||
|
plot_clamp_view_to_data(plot);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void plot_handle_mouse_wheel(Plot *plot, SDL_FRect outer, float mouse_x, float wheel_y) {
|
||||||
|
SDL_FRect inner = plot_inner_rect(outer);
|
||||||
|
double view_range = plot->view_max_x - plot->view_min_x;
|
||||||
|
double mouse_fraction = ((double)mouse_x - (double)inner.x) / (double)inner.w;
|
||||||
|
if (mouse_fraction < 0.0) mouse_fraction = 0.0;
|
||||||
|
if (mouse_fraction > 1.0) mouse_fraction = 1.0;
|
||||||
|
|
||||||
|
double anchor_x = plot->view_min_x + mouse_fraction * view_range;
|
||||||
|
double zoom_factor = wheel_y > 0.0f ? 0.8 : 1.25;
|
||||||
|
double new_range = view_range * zoom_factor;
|
||||||
|
|
||||||
|
plot->view_min_x = anchor_x - mouse_fraction * new_range;
|
||||||
|
plot->view_max_x = plot->view_min_x + new_range;
|
||||||
|
plot_clamp_view_to_data(plot);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void format_x_label(Plot *plot, double x, char *buffer, size_t buffer_size) {
|
||||||
|
if (plot->x_axis_is_time) {
|
||||||
|
time_t time = (time_t)x;
|
||||||
|
struct tm *tm = gmtime(&time);
|
||||||
|
if (tm != NULL) {
|
||||||
|
strftime(buffer, buffer_size, "%Y-%m", tm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(buffer, buffer_size, "%.2f", x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void plot_render(Plot *plot, SDL_Renderer *renderer, TTF_Font *font, SDL_FRect outer) {
|
||||||
|
SDL_FRect inner = plot_inner_rect(outer);
|
||||||
|
if (outer.w < 80.0f || outer.h < 80.0f || inner.w < 10.0f || inner.h < 10.0f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double visible_min_y = DBL_MAX;
|
||||||
|
double visible_max_y = -DBL_MAX;
|
||||||
|
for (int i = 0; i < plot->point_count; i += 1) {
|
||||||
|
PlotPoint point = plot->points[i];
|
||||||
|
if (point.x < plot->view_min_x || point.x > plot->view_max_x) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (point.y < visible_min_y) visible_min_y = point.y;
|
||||||
|
if (point.y > visible_max_y) visible_max_y = point.y;
|
||||||
|
}
|
||||||
|
if (visible_min_y == DBL_MAX || visible_min_y == visible_max_y) {
|
||||||
|
visible_min_y = plot->min_y;
|
||||||
|
visible_max_y = plot->max_y;
|
||||||
|
}
|
||||||
|
double y_padding = (visible_max_y - visible_min_y) * 0.05;
|
||||||
|
visible_min_y -= y_padding;
|
||||||
|
visible_max_y += y_padding;
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 15, 18, 26, 255);
|
||||||
|
SDL_RenderFillRect(renderer, &outer);
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 18, 24, 34, 255);
|
||||||
|
SDL_RenderFillRect(renderer, &inner);
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 45, 55, 70, 255);
|
||||||
|
for (int i = 0; i <= 4; i += 1) {
|
||||||
|
float y = inner.y + ((float)i / 4.0f) * inner.h;
|
||||||
|
SDL_RenderLine(renderer, inner.x, y, inner.x + inner.w, y);
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= 4; i += 1) {
|
||||||
|
float x = inner.x + ((float)i / 4.0f) * inner.w;
|
||||||
|
SDL_RenderLine(renderer, x, inner.y, x, inner.y + inner.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer, 190, 195, 205, 255);
|
||||||
|
SDL_RenderRect(renderer, &inner);
|
||||||
|
|
||||||
|
double x_range = plot->view_max_x - plot->view_min_x;
|
||||||
|
double y_range = visible_max_y - visible_min_y;
|
||||||
|
|
||||||
|
SDL_Rect clip = {
|
||||||
|
.x = (int)inner.x,
|
||||||
|
.y = (int)inner.y,
|
||||||
|
.w = (int)inner.w,
|
||||||
|
.h = (int)inner.h,
|
||||||
|
};
|
||||||
|
SDL_SetRenderClipRect(renderer, &clip);
|
||||||
|
SDL_SetRenderDrawColor(renderer, plot->line_color.r, plot->line_color.g, plot->line_color.b, plot->line_color.a);
|
||||||
|
for (int i = 1; i < plot->point_count; i += 1) {
|
||||||
|
PlotPoint a = plot->points[i - 1];
|
||||||
|
PlotPoint b = plot->points[i];
|
||||||
|
|
||||||
|
if ((a.x < plot->view_min_x && b.x < plot->view_min_x) ||
|
||||||
|
(a.x > plot->view_max_x && b.x > plot->view_max_x)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x1 = inner.x + (float)(((a.x - plot->view_min_x) / x_range) * inner.w);
|
||||||
|
float y1 = inner.y + inner.h - (float)(((a.y - visible_min_y) / y_range) * inner.h);
|
||||||
|
float x2 = inner.x + (float)(((b.x - plot->view_min_x) / x_range) * inner.w);
|
||||||
|
float y2 = inner.y + inner.h - (float)(((b.y - visible_min_y) / y_range) * inner.h);
|
||||||
|
SDL_RenderLine(renderer, x1, y1, x2, y2);
|
||||||
|
}
|
||||||
|
SDL_SetRenderClipRect(renderer, NULL);
|
||||||
|
|
||||||
|
SDL_Color title_color = {230, 235, 245, 255};
|
||||||
|
SDL_Color label_color = {175, 185, 200, 255};
|
||||||
|
SDL_Color axis_color = {130, 145, 165, 255};
|
||||||
|
|
||||||
|
draw_text(renderer, font, plot->title, outer.x + outer.w * 0.5f, outer.y + 6.0f, 0.5f, 0.0f, title_color);
|
||||||
|
draw_text(renderer, font, plot->y_axis_label, inner.x, outer.y + 8.0f, 0.0f, 0.0f, axis_color);
|
||||||
|
draw_text(renderer, font, plot->x_axis_label, inner.x + inner.w * 0.5f, inner.y + inner.h + 28.0f, 0.5f, 0.0f, axis_color);
|
||||||
|
|
||||||
|
char label[64];
|
||||||
|
for (int i = 0; i <= 4; i += 1) {
|
||||||
|
double value = visible_max_y - ((double)i / 4.0) * y_range;
|
||||||
|
float y = inner.y + ((float)i / 4.0f) * inner.h;
|
||||||
|
snprintf(label, sizeof(label), "%.2f", value);
|
||||||
|
draw_text(renderer, font, label, inner.x - 6.0f, y, 1.0f, 0.5f, label_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= 2; i += 1) {
|
||||||
|
double x_value = plot->view_min_x + ((double)i / 2.0) * x_range;
|
||||||
|
format_x_label(plot, x_value, label, sizeof(label));
|
||||||
|
float x = inner.x + ((float)i / 2.0f) * inner.w;
|
||||||
|
draw_text(renderer, font, label, x, inner.y + inner.h + 8.0f, 0.5f, 0.0f, label_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
b/src/plot.h
Normal file
35
b/src/plot.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#ifndef plot_h_INCLUDED
|
||||||
|
#define plot_h_INCLUDED
|
||||||
|
|
||||||
|
typedef struct PlotPoint {
|
||||||
|
double x;
|
||||||
|
double y;
|
||||||
|
} PlotPoint;
|
||||||
|
|
||||||
|
typedef struct Plot {
|
||||||
|
char title[64];
|
||||||
|
char x_axis_label[32];
|
||||||
|
char y_axis_label[32];
|
||||||
|
PlotPoint *points;
|
||||||
|
int point_count;
|
||||||
|
double min_x;
|
||||||
|
double max_x;
|
||||||
|
double min_y;
|
||||||
|
double max_y;
|
||||||
|
double view_min_x;
|
||||||
|
double view_max_x;
|
||||||
|
bool x_axis_is_time;
|
||||||
|
bool dragging;
|
||||||
|
SDL_Color line_color;
|
||||||
|
} Plot;
|
||||||
|
|
||||||
|
static bool point_in_rect(float x, float y, SDL_FRect rect);
|
||||||
|
static SDL_FRect plot_inner_rect(SDL_FRect outer);
|
||||||
|
static void plot_clamp_view_to_data(Plot *plot);
|
||||||
|
static void plot_init(Plot *plot, const char *title, const char *x_axis_label, const char *y_axis_label, PlotPoint *points, int point_count, double min_x, double max_x, double min_y, double max_y, bool x_axis_is_time, SDL_Color line_color);
|
||||||
|
static void plot_handle_mouse_motion(Plot *plot, SDL_FRect outer, float xrel);
|
||||||
|
static void plot_handle_mouse_wheel(Plot *plot, SDL_FRect outer, float mouse_x, float wheel_y);
|
||||||
|
static void plot_render(Plot *plot, SDL_Renderer *renderer, TTF_Font *font, SDL_FRect outer);
|
||||||
|
|
||||||
|
static void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, float x, float y, float anchor_x, float anchor_y, SDL_Color color);
|
||||||
|
#endif // plot_h_INCLUDED
|
||||||
Reference in New Issue
Block a user