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.
631 lines
14 KiB
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;
|
|
|
|
}
|