Commit 3769cd15 authored by Paul Asmuth's avatar Paul Asmuth
Browse files

add improved plot data loading routines

parent 8447b2fe
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -26,19 +26,19 @@ arguments:
    type: limit
    desc: |
      Set the axis value range to the closed interval [min, max].
      If no explicit maximm is specified, the default interval is [0.0, 1.0].
      If no explicit maximum is specified, the default interval is [0.0, 1.0].

  - name: limit-min
    type: limit_min
    desc: |
      Set the axis minimum value.
      If no explicit maximm is specified, the default maximum value is '0.0'.
      If no explicit maximum is specified, the default maximum value is '0.0'.

  - name: limit-max
    type: limit_max
    desc: |
      Set the axis maximum value.
      If no explicit maximm is specified, the default maximum value is '1.0'.
      If no explicit maximum is specified, the default maximum value is '1.0'.

  - name: label-placement
    type: label_placement
+8 −5
Original line number Diff line number Diff line
@@ -6,17 +6,20 @@ marked with the type `<measure>`.

The following typographic units are currently accepted:

  - `px` -- Pixels
  - `pt` -- Typographic points
  - `em` -- Typographic "em" size
  - `%`  -- Relative to the enclosing element
  - User units on an arbitrary scale
  - `px`  Pixels
  - `mm` – Millimeters
  - `pt` Typographic points
  - `em` – Typographic "em" size
  - `%`  – Relative to the enclosing element

#### Examples

    ;; 10 pixels
    10px

    ;; 40mm
    40mm

    ;; 16 points
    16pt

+190 −10
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@
 * limitations under the License.
 */
#include "data.h"
#include "utils/algo.h"
#include "utils/fileutil.h"
#include "utils/csv.h"
#include "utils/geojson.h"
@@ -22,6 +23,50 @@

namespace clip {

size_t databuf_len(const DataBuffer& buf) {
  return buf.index.size();
}

void databuf_add(DataBuffer* buf, double v) {
  buf->index.emplace_back(DataBuffer::Type::Number, buf->numeric.size());
  buf->numeric.push_back(v);
}

void databuf_add(DataBuffer* buf, const std::string& v) {
  buf->index.emplace_back(DataBuffer::Type::Text, buf->text.size());
  buf->text.emplace_back(v);
}

void databuf_to_text(
    const DataBuffer& buf,
    std::vector<std::string>* values) {
  for (const auto& [type, pos] : buf.index) {
    switch (type) {
      case DataBuffer::Type::Number:
        values->push_back(std::to_string(buf.numeric[pos]));
        break;
      case DataBuffer::Type::Text:
        values->push_back(buf.text[pos]);
        break;
    }
  }
}

void databuf_to_numbers(
    const DataBuffer& buf,
    std::vector<double>* values) {
  for (const auto& [type, pos] : buf.index) {
    switch (type) {
      case DataBuffer::Type::Number:
        values->push_back(buf.numeric[pos]);
        break;
      case DataBuffer::Type::Text:
        values->push_back(parse_f64(buf.text[pos]));
        break;
    }
  }
}

size_t series_len(const Series& s) {
  return s.size();
}
@@ -185,9 +230,94 @@ ReturnCode data_load_strings(
  return expr_to_strings(expr, values);
}

ReturnCode data_load_simple_inline(
    const Expr* expr,
    DataBuffer* values) {
  expr_each(expr, [values] (const auto& e) {
    databuf_add(values, expr_get_value(e));
  });

  return OK;
}

ReturnCode data_load_simple_csv(
    const Expr* expr,
    DataBuffer* values) {
  auto args = expr_collect(expr);
  if (args.size() != 2 ||
      !expr_is_value(args[0]) ||
      !expr_is_value(args[1])) {
    return errorf(
        ERROR,
        "invalid number of arguments to 'csv'; expected: 2, got: {}",
        expr_inspect_list(expr));
  }

  const auto& path = expr_get_value(args[0]);
  const auto& column_name = expr_get_value(args[1]);

  std::string data_str;
  if (auto rc = read_file(path, &data_str); !rc) {
    return rc;
  }

  auto data_csv = CSVData{};
  if (auto rc = csv_parse(data_str, &data_csv); !rc) {
    return rc;
  }

  if (data_csv.empty()) {
    return OK;
  }

  const auto& headers = data_csv.front();
  const auto& header = std::find(headers.begin(), headers.end(), column_name);
  if (header == headers.end()) {
    return errorf(
        ERROR,
        "CSV column not found: {}",
        column_name);
  }

  auto column_idx = std::distance(headers.begin(), header);
  for (auto row = ++data_csv.begin(); row != data_csv.end(); ++row) {
    if (row->size() < column_idx) {
      return errorf(
          ERROR,
          "CSV invalid number of columns for row #{}",
          std::distance(data_csv.begin(), row));
    }

    databuf_add(values, row->at(column_idx));
  }

  return OK;
}

ReturnCode data_load_simple(
    const Expr* expr,
    DataBuffer* values) {
  if (!expr || !expr_is_list(expr)) {
    return errorf(
        ERROR,
        "argument error; expected a list, got: {}",
        expr_inspect(expr));
  }

  auto args = expr_get_list(expr);

  if (args && expr_is_value_literal(args, "csv")) {
    return data_load_simple_csv(expr_next(args), values);
  }

  return data_load_simple_inline(args, values);
}

ReturnCode data_load_polylines2_geojson(
    const Expr* expr,
    std::vector<PolyLine2>* data) {
    DataBuffer* data_x,
    DataBuffer* data_y,
    std::vector<size_t>* index) {
  if (!expr || !expr_is_value(expr)) {
    return errorf(
        ERROR,
@@ -198,9 +328,17 @@ ReturnCode data_load_polylines2_geojson(
  const auto& path = expr_get_value(expr);

  GeoJSONReader reader;
  reader.on_lines = [data] (const PolyLine3* polys, size_t poly_count) {
  reader.on_lines = [data_x, data_y, index] (const PolyLine3* polys, size_t poly_count) {
    for (size_t i = 0; i < poly_count; ++i) {
      data->emplace_back(polyline3_to_polyline2(polys[i]));
      auto len = std::min(databuf_len(*data_x), databuf_len(*data_y));
      if (len > 0) {
        index->push_back(len);
      }

      for (const auto& v : polys[i].vertices) {
        databuf_add(data_x, v.x);
        databuf_add(data_y, v.y);
      }
    }

    return OK;
@@ -211,7 +349,9 @@ ReturnCode data_load_polylines2_geojson(

ReturnCode data_load_polylines2(
    const Expr* expr,
    std::vector<PolyLine2>* data) {
    DataBuffer* data_x,
    DataBuffer* data_y,
    std::vector<size_t>* index) {
  if (!expr || !expr_is_list(expr) || !expr_get_list(expr)) {
    return errorf(
        ERROR,
@@ -222,7 +362,7 @@ ReturnCode data_load_polylines2(
  auto args = expr_get_list(expr);

  if (args && expr_is_value_literal(args, "geojson")) {
    return data_load_polylines2_geojson(expr_next(args), data);
    return data_load_polylines2_geojson(expr_next(args), data_x, data_y, index);
  }

  return err_invalid_value(expr_inspect(expr), {
@@ -277,7 +417,8 @@ ReturnCode data_load_polys2(

ReturnCode data_load_points2_geojson(
    const Expr* expr,
    std::vector<vec2>* data) {
    DataBuffer* data_x,
    DataBuffer* data_y) {
  if (!expr || !expr_is_value(expr)) {
    return errorf(
        ERROR,
@@ -288,9 +429,10 @@ ReturnCode data_load_points2_geojson(
  const auto& path = expr_get_value(expr);

  GeoJSONReader reader;
  reader.on_points = [data] (const vec3* points, size_t point_count) {
  reader.on_points = [data_x, data_y] (const vec3* points, size_t point_count) {
    for (size_t i = 0; i < point_count; ++i) {
      data->emplace_back(points[i]);
      databuf_add(data_x, points[i].x);
      databuf_add(data_y, points[i].y);
    }

    return OK;
@@ -301,7 +443,8 @@ ReturnCode data_load_points2_geojson(

ReturnCode data_load_points2(
    const Expr* expr,
    std::vector<vec2>* data) {
    DataBuffer* data_x,
    DataBuffer* data_y) {
  if (!expr || !expr_is_list(expr) || !expr_get_list(expr)) {
    return errorf(
        ERROR,
@@ -312,7 +455,7 @@ ReturnCode data_load_points2(
  auto args = expr_get_list(expr);

  if (args && expr_is_value_literal(args, "geojson")) {
    return data_load_points2_geojson(expr_next(args), data);
    return data_load_points2_geojson(expr_next(args), data_x, data_y);
  }

  return err_invalid_value(expr_inspect(expr), {
@@ -376,6 +519,43 @@ ReturnCode data_to_measures(
  return OK;
}

ReturnCode data_parse(
    std::vector<std::string>& src,
    const ScaleConfig& scale,
    std::vector<double>* dst) {
  for (auto v = src.begin(); v != src.end(); ++v) {
    double x;
    switch (scale.kind) {
      case ScaleKind::CATEGORICAL: {
        auto v_iter = scale.categories_map.find(*v);
        if (v_iter == scale.categories_map.end()) {
          return errorf(
              ERROR,
              "error while parsing data: value '{}' is not part of the categories list",
              *v);
        }

        x = scale_translate_categorical(scale, v_iter->second);
        break;
      }
      default:
        try {
          x = std::stod(*v);
        } catch (...) {
          return errorf(
              ERROR,
              "error while parsing data: '{}' as string; "
              "if this is intentional, set 'scale-[x,y] to (categorical ...)'",
              *v);
        }
        break;
    }

    dst->push_back(x);
  }

  return OK;
}

} // namespace clip
+34 −2
Original line number Diff line number Diff line
@@ -31,6 +31,26 @@ struct DataGroup {
  std::vector<size_t> index;
};

struct DataBuffer {
  enum class Type { Number, Text };
  std::vector<std::tuple<Type, size_t>> index;
  std::vector<double> numeric;
  std::vector<std::string> text;
};

size_t databuf_len(const DataBuffer& buf);

void databuf_add(DataBuffer* buf, double v);
void databuf_add(DataBuffer* buf, const std::string& v);

void databuf_to_text(
    const DataBuffer& buf,
    std::vector<std::string>* values);

void databuf_to_numbers(
    const DataBuffer& buf,
    std::vector<double>* values);

std::vector<DataGroup> series_group(const Series& data);

size_t series_len(const Series& s);
@@ -52,9 +72,15 @@ ReturnCode data_load_strings(
    const Expr* expr,
    std::vector<std::string>* values);

ReturnCode data_load_simple(
    const Expr* expr,
    DataBuffer* values);

ReturnCode data_load_polylines2(
    const Expr* expr,
    std::vector<PolyLine2>* data);
    DataBuffer* data_x,
    DataBuffer* data_y,
    std::vector<size_t>* index);

ReturnCode data_load_polys2(
    const Expr* expr,
@@ -62,7 +88,8 @@ ReturnCode data_load_polys2(

ReturnCode data_load_points2(
    const Expr* expr,
    std::vector<vec2>* data);
    DataBuffer* data_x,
    DataBuffer* data_y);

ReturnCode data_load(
    const Expr* expr,
@@ -79,6 +106,11 @@ ReturnCode data_to_measures(
    const ScaleConfig& scale,
    std::vector<Measure>* dst);

ReturnCode data_parse(
    std::vector<std::string>& src,
    const ScaleConfig& scale,
    std::vector<double>* dst);

} // namespace clip

#include "data_impl.h"
+102 −157
Original line number Diff line number Diff line
@@ -37,10 +37,10 @@ namespace clip::plotgen {
struct PlotAreaConfig {
  PlotAreaConfig();
  Direction direction;
  std::vector<Measure> x;
  std::vector<Measure> xoffset;
  std::vector<Measure> y;
  std::vector<Measure> yoffset;
  DataBuffer x;
  DataBuffer xoffset;
  DataBuffer y;
  DataBuffer yoffset;
  ScaleConfig scale_x;
  ScaleConfig scale_y;
  StrokeStyle stroke_high_style;
@@ -54,55 +54,43 @@ PlotAreaConfig::PlotAreaConfig() :
ReturnCode areas_draw_horizontal(
    Context* ctx,
    PlotConfig* plot,
    PlotAreaConfig config) {
    const PlotAreaConfig& conf) {
  const auto& clip = plot_get_clip(plot, layer_get(ctx));

  /* convert units */
  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_x), _1),
        std::bind(&convert_unit_relative, clip.w, _1)
      },
      &*config.x.begin(),
      &*config.x.end());
  /* transform data */
  std::vector<double> xs;
  if (auto rc = scale_translatev(conf.scale_x, conf.x, &xs); !rc) {
    return rc;
  }

  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_x), _1),
        std::bind(&convert_unit_relative, clip.w, _1)
      },
      &*config.xoffset.begin(),
      &*config.xoffset.end());
  std::vector<double> xoffsets;
  if (auto rc = scale_translatev(conf.scale_x, conf.xoffset, &xoffsets); !rc) {
    return rc;
  }

  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_y), _1),
        std::bind(&convert_unit_relative, clip.h, _1)
      },
      &*config.y.begin(),
      &*config.y.end());
  std::vector<double> ys;
  if (auto rc = scale_translatev(conf.scale_y, conf.y, &ys); !rc) {
    return rc;
  }

  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_y), _1),
        std::bind(&convert_unit_relative, clip.h, _1)
      },
      &*config.yoffset.begin(),
      &*config.yoffset.end());
  std::vector<double> yoffsets;
  if (auto rc = scale_translatev(conf.scale_y, conf.yoffset, &yoffsets); !rc) {
    return rc;
  }

  /* draw areas */
  DrawCommand shape;
  shape.fill_style = config.fill_style;
  shape.fill_style = conf.fill_style;

  DrawCommand stroke_high;
  stroke_high.stroke_style = config.stroke_high_style;
  stroke_high.stroke_style = conf.stroke_high_style;

  DrawCommand stroke_low;
  stroke_low.stroke_style = config.stroke_low_style;
  stroke_low.stroke_style = conf.stroke_low_style;

  for (size_t i = 0; i < config.x.size(); ++i) {
    auto sx = clip.x + config.x[i];
    auto sy = clip.y + config.y[i];
  for (size_t i = 0; i < xs.size(); ++i) {
    auto sx = clip.x + xs[i] * clip.w;
    auto sy = clip.y + ys[i] * clip.h;

    if (i == 0) {
      shape.path.moveTo(sx, sy);
@@ -113,10 +101,10 @@ ReturnCode areas_draw_horizontal(
    }
  }

  auto x0 = clip.h * std::clamp(scale_translate(config.scale_x, 0), 0.0, 1.0);
  for (int i = config.x.size() - 1; i >= 0; --i) {
    auto sx = clip.x + (config.xoffset.empty() ? x0 : config.xoffset[i]);
    auto sy = clip.y + config.y[i];
  auto x0 = std::clamp(scale_translate(conf.scale_x, 0), 0.0, 1.0);
  for (int i = xs.size() - 1; i >= 0; --i) {
    auto sx = clip.x + (xoffsets.empty() ? x0 : xoffsets[i]) * clip.w;
    auto sy = clip.y + ys[i] * clip.h;
    shape.path.lineTo(sx, sy);

    if (stroke_low.path.empty()) {
@@ -138,55 +126,43 @@ ReturnCode areas_draw_horizontal(
ReturnCode areas_draw_vertical(
    Context* ctx,
    PlotConfig* plot,
    PlotAreaConfig config) {
    PlotAreaConfig conf) {
  const auto& clip = plot_get_clip(plot, layer_get(ctx));

  /* convert units */
  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_x), _1),
        std::bind(&convert_unit_relative, clip.w, _1)
      },
      &*config.x.begin(),
      &*config.x.end());
  /* transform data */
  std::vector<double> xs;
  if (auto rc = scale_translatev(conf.scale_x, conf.x, &xs); !rc) {
    return rc;
  }

  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_x), _1),
        std::bind(&convert_unit_relative, clip.w, _1)
      },
      &*config.xoffset.begin(),
      &*config.xoffset.end());
  std::vector<double> xoffsets;
  if (auto rc = scale_translatev(conf.scale_x, conf.xoffset, &xoffsets); !rc) {
    return rc;
  }

  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_y), _1),
        std::bind(&convert_unit_relative, clip.h, _1)
      },
      &*config.y.begin(),
      &*config.y.end());
  std::vector<double> ys;
  if (auto rc = scale_translatev(conf.scale_y, conf.y, &ys); !rc) {
    return rc;
  }

  convert_units(
      {
        std::bind(&convert_unit_user, scale_translate_fn(config.scale_y), _1),
        std::bind(&convert_unit_relative, clip.h, _1)
      },
      &*config.yoffset.begin(),
      &*config.yoffset.end());
  std::vector<double> yoffsets;
  if (auto rc = scale_translatev(conf.scale_y, conf.yoffset, &yoffsets); !rc) {
    return rc;
  }

  /* draw areas */
  DrawCommand shape;
  shape.fill_style = config.fill_style;
  shape.fill_style = conf.fill_style;

  DrawCommand stroke_high;
  stroke_high.stroke_style = config.stroke_high_style;
  stroke_high.stroke_style = conf.stroke_high_style;

  DrawCommand stroke_low;
  stroke_low.stroke_style = config.stroke_low_style;
  stroke_low.stroke_style = conf.stroke_low_style;

  for (size_t i = 0; i < config.x.size(); ++i) {
    auto sx = clip.x + config.x[i];
    auto sy = clip.y + config.y[i];
  for (size_t i = 0; i < xs.size(); ++i) {
    auto sx = clip.x + xs[i] * clip.w;
    auto sy = clip.y + ys[i] * clip.h;

    if (i == 0) {
      shape.path.moveTo(sx, sy);
@@ -197,10 +173,10 @@ ReturnCode areas_draw_vertical(
    }
  }

  auto y0 = clip.h * std::clamp(scale_translate(config.scale_y, 0), 0.0, 1.0);
  for (int i = config.x.size() - 1; i >= 0; --i) {
    auto sx = clip.x + config.x[i];
    auto sy = clip.y + (config.yoffset.empty() ? y0 : config.yoffset[i]);
  auto y0 = std::clamp(scale_translate(conf.scale_y, 0), 0.0, 1.0);
  for (int i = xs.size() - 1; i >= 0; --i) {
    auto sx = clip.x + xs[i] * clip.w;
    auto sy = clip.y + (yoffsets.empty() ? y0 : yoffsets[i]) * clip.h;
    shape.path.lineTo(sx, sy);

    if (stroke_low.path.empty()) {
@@ -234,18 +210,14 @@ ReturnCode areas_configure(
  c->fill_style.color = layer_get(ctx)->foreground_color;

  /* parse properties */
  std::vector<std::string> data_x;
  std::vector<std::string> data_y;
  std::vector<std::string> data_xoffset;
  std::vector<std::string> data_yoffset;

  auto config_rc = expr_walk_map_wrapped(expr, {
    {"data-x", std::bind(&data_load, _1, &c->x)},
    {"data-y", std::bind(&data_load, _1, &c->y)},
    {"data-x-high", std::bind(&data_load, _1, &c->x)},
    {"data-y-high", std::bind(&data_load, _1, &c->y)},
    {"data-x-low", std::bind(&data_load, _1, &c->xoffset)},
    {"data-y-low", std::bind(&data_load, _1, &c->yoffset)},
    {"data", std::bind(&data_load_points2, _1, &c->x, &c->y)},
    {"data-x", std::bind(&data_load_simple, _1, &c->x)},
    {"data-y", std::bind(&data_load_simple, _1, &c->y)},
    {"data-x-high", std::bind(&data_load_simple, _1, &c->x)},
    {"data-y-high", std::bind(&data_load_simple, _1, &c->y)},
    {"data-x-low", std::bind(&data_load_simple, _1, &c->xoffset)},
    {"data-y-low", std::bind(&data_load_simple, _1, &c->yoffset)},
    {"limit-x", std::bind(&expr_to_float64_opt_pair, _1, &c->scale_x.min, &c->scale_x.max)},
    {"limit-x-min", std::bind(&expr_to_float64_opt, _1, &c->scale_x.min)},
    {"limit-x-max", std::bind(&expr_to_float64_opt, _1, &c->scale_x.max)},
@@ -305,66 +277,19 @@ ReturnCode areas_configure(
    return config_rc;
  }

  /* scale configuration */
  if (auto rc = data_to_measures(data_x, c->scale_x, &c->x); !rc){
    return rc;
  }

  if (auto rc = data_to_measures(data_xoffset, c->scale_x, &c->xoffset); !rc){
    return rc;
  }

  if (auto rc = data_to_measures(data_y, c->scale_y, &c->y); !rc){
    return rc;
  }

  if (auto rc = data_to_measures(data_yoffset, c->scale_y, &c->yoffset); !rc){
    return rc;
  }

  for (const auto& v : c->x) {
    if (v.unit == Unit::USER) {
      scale_fit(v.value, &c->scale_x);
    }
  }

  for (const auto& v : c->xoffset) {
    if (v.unit == Unit::USER) {
      scale_fit(v.value, &c->scale_x);
    }
  }

  for (const auto& v : c->y) {
    if (v.unit == Unit::USER) {
      scale_fit(v.value, &c->scale_y);
    }
  }

  for (const auto& v : c->yoffset) {
    if (v.unit == Unit::USER) {
      scale_fit(v.value, &c->scale_y);
    }
  }

  /* check configuraton */
  if (c->x.size() != c->y.size()) {
    return error(
        ERROR,
        "the length of the 'data-x' and 'data-y' properties must be equal");
  if (databuf_len(c->x) != databuf_len(c->y)) {
    return error(ERROR, "The length of the 'data-x' and 'data-y' lists must be equal");
  }

  if (!c->xoffset.empty() &&
      c->xoffset.size() != c->x.size()) {
    return error(
        ERROR,
        "the length of the 'data-x' and 'data-x-low' properties must be equal");
  if (databuf_len(c->xoffset) != 0 &&
      databuf_len(c->x) != databuf_len(c->xoffset)) {
    return error(ERROR, "the length of the 'data-x' and 'data-x-low' properties must be equal");
  }

  if (!c->yoffset.empty() &&
      c->yoffset.size() != c->y.size()) {
    return error(
        ERROR,
        "the length of the 'data-y' and 'data-y-low' properties must be equal");
  if (databuf_len(c->yoffset) != 0 &&
      databuf_len(c->y) != databuf_len(c->yoffset)) {
    return error(ERROR, "the length of the 'data-y' and 'data-y-low' properties must be equal");
  }

  return OK;
@@ -395,7 +320,27 @@ ReturnCode areas_autorange(
    PlotConfig* plot,
    const Expr* expr) {
  PlotAreaConfig conf;
  return areas_configure(ctx, plot, &conf, expr);
  if (auto rc = areas_configure(ctx, plot, &conf, expr); !rc) {
    return rc;
  }

  if (auto rc = scale_fit(&plot->scale_x, conf.x); !rc) {
    return rc;
  }

  if (auto rc = scale_fit(&plot->scale_x, conf.xoffset); !rc) {
    return rc;
  }

  if (auto rc = scale_fit(&plot->scale_y, conf.y); !rc) {
    return rc;
  }

  if (auto rc = scale_fit(&plot->scale_y, conf.yoffset); !rc) {
    return rc;
  }

  return OK;
}

} // namespace clip::plotgen
Loading