You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
netsurf/content/fetchers/about/chart.c

631 lines
14 KiB

/*
* Copyright 2020 Vincent Sanders <vince@netsurf-browser.org>
*
* This file is part of NetSurf.
*
* NetSurf is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* NetSurf is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* \file
* content generator for the about scheme chart page
*
* A chart consists of the figure area in which a chart a title and a
* key are placed.
*
*
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include "utils/config.h"
#include "netsurf/inttypes.h"
#include "utils/config.h"
#include "utils/utils.h"
#include "utils/errors.h"
#include "utils/nsurl.h"
#include "private.h"
#include "chart.h"
/** minimum figure dimension */
#define FIGURE_MIN_WIDTH 150
#define FIGURE_MIN_HEIGHT 100
enum chart_type {
CHART_TYPE_UNKNOWN,
CHART_TYPE_PIE,
};
/* type of chart key */
enum key_type {
CHART_KEY_UNSET,
CHART_KEY_NONE,
CHART_KEY_LEFT,
CHART_KEY_RIGHT,
CHART_KEY_TOP,
CHART_KEY_BOT,
CHART_KEY_END
};
struct chart_label {
char *title; /* label title */
unsigned int colour; /* colour */
};
struct chart_series {
unsigned int len; /* number of values in the series */
float *value; /* array of values */
};
#define MAX_SERIES 4
struct chart_data {
unsigned int series_len;
struct chart_series series[MAX_SERIES];
unsigned int label_len; /* number of labels */
struct chart_label *label;
};
/**
* parameters for a chart figure
*/
struct chart_param {
enum chart_type type;
enum key_type key; /* what type of key to use */
unsigned int width; /* width of figure */
unsigned int height; /* height of figure */
char *title; /* title */
struct {
unsigned int x;
unsigned int y;
unsigned int width;
unsigned int height;
} area; /* chart area within figure */
struct chart_data data;
};
#define DEF_COLOUR_NUM 8
/** default colour series */
static unsigned int colour_series[DEF_COLOUR_NUM] =
{
0x00ff00, /* green */
0x0000ff, /* blue */
0xff0000, /* red */
0xffff00, /* yellow */
0x00ffff, /* cyan */
0xff00ff, /* pink */
0x777777, /* grey */
0x000000, /* black */
};
/* ensures there are labels present for every value */
static nserror ensure_label_count(struct chart_param *chart, unsigned int count)
{
unsigned int lidx;
int deltac;
struct chart_label *nlabels;
deltac = count - chart->data.label_len;
if (deltac <= 0) {
/* there are enough labels */
return NSERROR_OK;
}
nlabels = realloc(chart->data.label,
count * sizeof(struct chart_label));
if (nlabels == NULL) {
return NSERROR_NOMEM;
}
chart->data.label = nlabels;
for (lidx = chart->data.label_len; lidx < count; lidx++) {
chart->data.label[lidx].title = calloc(1, 20);
snprintf(chart->data.label[lidx].title, 19, "item %d", lidx + 1);
chart->data.label[lidx].colour = colour_series[lidx % DEF_COLOUR_NUM];
}
chart->data.label_len = count;
return NSERROR_OK;
}
/**
* extract values for a series
*/
static nserror
extract_series_values(struct chart_param *chart,
unsigned int series_num,
const char *valstr,
size_t valstrlen)
{
nserror res;
unsigned int valcur;
size_t valstart;/* value start in valstr */
size_t vallen; /* value end in valstr */
struct chart_series *series;
series = chart->data.series + series_num;
/* ensure we do not leak any data in this series */
if (series->value != NULL) {
free(series->value);
}
/* count how many values present */
for (series->len = 1, valstart=0; valstart < valstrlen; valstart++) {
if (valstr[valstart] == ',') {
series->len++;
}
}
/* allocate storage for values */
series->value = calloc(series->len, sizeof(float));
if (series->value == NULL) {
return NSERROR_NOMEM;
}
/* extract values from query string */
for (valcur = 0, vallen = 0, valstart = 0;
(valstart < valstrlen) && (valcur < series->len);
valstart += vallen, valcur++) {
/* get query section length */
vallen = 0;
while (((valstart + vallen) < valstrlen) &&
(valstr[valstart + vallen] != ',')) {
vallen++;
}
series->value[valcur] = strtof(valstr + valstart, NULL);
vallen++; /* account for , separator */
}
res = ensure_label_count(chart, series->len);
return res;
}
/**
* extract values for next series
*/
static nserror
extract_next_series_values(struct chart_param *chart,
const char *valstr,
size_t valstrlen)
{
nserror res;
if (chart->data.series_len >= MAX_SERIES) {
return NSERROR_NOSPACE;
}
res = extract_series_values(chart,
chart->data.series_len,
valstr,
valstrlen);
if (res == NSERROR_OK) {
chart->data.series_len++;
}
return res;
}
/**
* extract label title
*/
static nserror
extract_series_labels(struct chart_param *chart,
const char *valstr,
size_t valstrlen)
{
nserror res;
unsigned int valcount; /* count of values in valstr */
unsigned int valcur;
size_t valstart;/* value start in valstr */
size_t vallen; /* value end in valstr */
for (valcount = 1, valstart=0; valstart < valstrlen; valstart++) {
if (valstr[valstart] == ',') {
valcount++;
}
}
res = ensure_label_count(chart, valcount);
if (res != NSERROR_OK) {
return res;
}
for (valcur = 0, vallen = 0, valstart = 0;
(valstart < valstrlen) && (valcur < chart->data.label_len);
valstart += vallen, valcur++) {
/* get query section length */
vallen = 0;
while (((valstart + vallen) < valstrlen) &&
(valstr[valstart + vallen] != ',')) {
vallen++;
}
chart->data.label[valcur].title = strndup(valstr + valstart, vallen);
vallen++; /* account for , separator */
}
return NSERROR_OK;
}
/**
* extract labels colour
*/
static nserror
extract_series_colours(struct chart_param *chart,
const char *valstr,
size_t valstrlen)
{
return NSERROR_OK;
}
/**
* process a part of a query
*/
static nserror
process_query_section(const char *str, size_t len, struct chart_param *chart)
{
nserror res = NSERROR_OK;
if ((len > 6) &&
(strncmp(str, "width=", 6) == 0)) {
/* figure width */
chart->width = strtoul(str + 6, NULL, 10);
} else if ((len > 7) &&
(strncmp(str, "height=", 7) == 0)) {
/* figure height */
chart->height = strtoul(str + 7, NULL, 10);
} else if ((len > 8) &&
(strncmp(str, "cawidth=", 8) == 0)) {
/* chart area width */
chart->area.width = strtoul(str + 8, NULL, 10);
} else if ((len > 9) &&
(strncmp(str, "caheight=", 9) == 0)) {
/* chart area height */
chart->area.height = strtoul(str + 9, NULL, 10);
} else if ((len > 4) &&
(strncmp(str, "key=", 4) == 0)) {
/* figure has key */
chart->key = strtoul(str + 4, NULL, 10);
} else if ((len > 6) &&
(strncmp(str, "title=", 6) == 0)) {
chart->title = strndup(str + 6, len - 6);
} else if ((len > 5) &&
(strncmp(str, "type=", 5) == 0)) {
if (strncmp(str + 5, "pie", len - 5) == 0) {
chart->type = CHART_TYPE_PIE;
} else {
chart->type = CHART_TYPE_UNKNOWN;
}
} else if ((len > 7) &&
(strncmp(str, "values=", 7) == 0)) {
res = extract_next_series_values(chart, str + 7, len - 7);
} else if ((len > 7) &&
(strncmp(str, "labels=", 7) == 0)) {
res = extract_series_labels(chart, str + 7, len - 7);
} else if ((len > 8) &&
(strncmp(str, "colours=", 8) == 0)) {
res = extract_series_colours(chart, str + 8, len - 8);
}
return res;
}
static nserror
chart_from_query(struct nsurl *url, struct chart_param *chart)
{
nserror res;
char *querystr;
size_t querylen;
size_t kvstart;/* key value start */
size_t kvlen; /* key value end */
res = nsurl_get(url, NSURL_QUERY, &querystr, &querylen);
if (res != NSERROR_OK) {
return res;
}
for (kvlen = 0, kvstart = 0; kvstart < querylen; kvstart += kvlen) {
/* get query section length */
kvlen = 0;
while (((kvstart + kvlen) < querylen) &&
(querystr[kvstart + kvlen] != '&')) {
kvlen++;
}
res = process_query_section(querystr + kvstart, kvlen, chart);
if (res != NSERROR_OK) {
break;
}
kvlen++; /* account for & separator */
}
free(querystr);
/* sanity check dimensions */
if (chart->width < FIGURE_MIN_WIDTH) {
/* bad width - check height */
if (chart->height < FIGURE_MIN_HEIGHT) {
/* both bad set to defaults */
chart->width = FIGURE_MIN_WIDTH;
chart->height = FIGURE_MIN_HEIGHT;
} else {
/* base width on valid height */
chart->width = (chart->height * 3) / 2;
}
} else {
/* good width check height */
if (chart->height < FIGURE_MIN_HEIGHT) {
/* base height on valid width */
chart->height = (chart->width * 2) / 3;
}
}
/* ensure legend type correct */
if ((chart->key == CHART_KEY_UNSET) ||
(chart->key >= CHART_KEY_END )) {
/* default to putting key on right */
chart->key = CHART_KEY_RIGHT;
}
return NSERROR_OK;
}
static nserror
output_pie_legend(struct fetch_about_context *ctx, struct chart_param *chart)
{
nserror res;
unsigned int lblidx;
unsigned int legend_width;
unsigned int legend_height;
unsigned int vertical_spacing;
switch (chart->key) {
case CHART_KEY_NONE:
break;
case CHART_KEY_RIGHT:
legend_width = chart->width - chart->area.width - chart->area.x;
legend_width -= 10; /* margin */
legend_height = chart->height;
vertical_spacing = legend_height / (chart->data.label_len + 1);
for(lblidx = 0; lblidx < chart->data.label_len ; lblidx++) {
res = fetch_about_ssenddataf(ctx,
"<rect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"#%06x\" />",
chart->width - legend_width,
(vertical_spacing * lblidx) + (vertical_spacing/2),
vertical_spacing * 2 / 3,
vertical_spacing * 2 / 3,
chart->data.label[lblidx].colour);
if (res != NSERROR_OK) {
return res;
}
res = fetch_about_ssenddataf(ctx,
"<text x=\"%d\" y=\"%d\" fill=\"#%06x\" >%s</text>",
chart->width - legend_width + vertical_spacing,
vertical_spacing * (lblidx+1),
chart->data.label[lblidx].colour,
chart->data.label[lblidx].title);
if (res != NSERROR_OK) {
return res;
}
}
break;
default:
break;
}
return NSERROR_OK;
}
static float
compute_series_total(struct chart_param *chart, unsigned int series)
{
float total;
unsigned int curdata;
for (total = 0, curdata = 0;
curdata < chart->data.series[series].len;
curdata++) {
total += chart->data.series[series].value[curdata];
}
return total;
}
/**
* render the data as a pie chart svg
*/
static bool
pie_chart(struct fetch_about_context *ctx, struct chart_param *chart)
{
nserror res;
float ra; /* pie a radius */
float rb; /* pie b radius */
float series_total;
unsigned int curdata; /* current data point index */
float last_x, last_y;
float end_x, end_y;
float start;
float extent;
bool large;
float circle_centre_x, circle_centre_y;
/* ensure there is data to render */
if ((chart->data.series_len < 1) || (chart->data.series[0].len < 2)) {
return NSERROR_BAD_PARAMETER;
}
/* get the first series total value */
series_total = compute_series_total(chart, 0);
if (series_total == 0) {
/* dividing by zero is embarasing */
return NSERROR_BAD_PARAMETER;
}
/*
* need to ensure the chart area is setup correctly
*
* this is left to each chart type as different charts
* have differnt requirements
*/
if ((chart->area.width == 0) || (chart->area.height == 0)) {
/*
* pie chart defaults to square of smaller of figure
* width and height
*/
if (chart->width > chart->height) {
chart->area.width = chart->area.height = (chart->height - chart->area.x);
} else {
chart->area.width = chart->area.height = (chart->width - chart->area.y);
}
}
/* content is going to return ok */
fetch_about_set_http_code(ctx, 200);
/* content type */
if (fetch_about_send_header(ctx,
"Content-Type: image/svg; charset=utf-8")) {
goto aborted;
}
/* get the pie charts elipse radii */
ra = chart->area.width / 2;
rb = chart->area.height / 2;
/* get the offset to the circle centre */
circle_centre_x = chart->area.x + ra;
circle_centre_y = chart->area.y + rb;
/* svg header */
res = fetch_about_ssenddataf(ctx,
"<svg width=\"%u\" height=\"%u\" "
"xmlns=\"http://www.w3.org/2000/svg\">\n",
chart->width, chart->height);
if (res != NSERROR_OK) {
goto aborted;
}
/* generate the legend */
res = output_pie_legend(ctx, chart);
if (res != NSERROR_OK) {
goto aborted;
}
/* plot the arcs */
start = -M_PI_2;
last_x = (ra * cos(start));
last_y = (rb * sin(start));
/* iterate over each data point creating a slice o pie */
for (curdata=0; curdata < chart->data.series[0].len; curdata++) {
extent = ((chart->data.series[0].value[curdata] / series_total) * 2 * M_PI);
end_x = (ra * cos(start + extent));
end_y = (rb * sin(start + extent));
if (extent > M_PI) {
large = true;
} else {
large = false;
}
res = fetch_about_ssenddataf(
ctx,
"<path d=\"M %g %g\n"
"A %g %g 0 %d 1 %g %g\n"
"L %g %g Z\" fill=\"#%06x\" />\n",
circle_centre_x + last_x,
circle_centre_y + last_y,
ra, rb, large?1:0,
circle_centre_x + end_x,
circle_centre_y + end_y,
circle_centre_x,
circle_centre_y,
chart->data.label[curdata].colour);
if (res != NSERROR_OK) {
goto aborted;
}
last_x = end_x;
last_y = end_y;
start +=extent;
}
res = fetch_about_ssenddataf(ctx, "</svg>\n");
if (res != NSERROR_OK) {
goto aborted;
}
fetch_about_send_finished(ctx);
return true;
aborted:
return false;
}
/**
* Handler to generate about scheme chart page.
*
* generates an svg chart
*
* \param ctx The fetcher context.
* \return true if handled false if aborted.
*/
bool fetch_about_chart_handler(struct fetch_about_context *ctx)
{
nserror res;
struct chart_param chart;
memset(&chart, 0, sizeof(struct chart_param));
res = chart_from_query(fetch_about_get_url(ctx), &chart);
if (res != NSERROR_OK) {
goto aborted;
}
switch (chart.type) {
case CHART_TYPE_PIE:
return pie_chart(ctx, &chart);
default:
break;
}
aborted:
return false;
}