Skip to content

Commit

Permalink
Merge pull request #558 from pangenome/downsample_nodes_in_svg
Browse files Browse the repository at this point in the history
`odgi draw`: add node sparsification factor for SVG output
  • Loading branch information
AndreaGuarracino committed Feb 13, 2024
2 parents d42a868 + 2cd6e57 commit ff88bfc
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 24 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ docs/sphinx_build
docs/sphinx_build_man
docs/_build
Testing/
.idea/
.idea/
.vscode/
.cmake/
cmake-build-debug/
129 changes: 110 additions & 19 deletions src/algorithms/draw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,91 @@ bool is_too_close(double x, double y, const std::string& content, double thresho
}
return false;
}

uint64_t node_hash(const nid_t& node_id) {
uint64_t x = node_id;
x = (~x) + (x << 21); // x = (x << 21) - x - 1;
x = x ^ (x >> 24);
x = (x + (x << 3)) + (x << 8); // x * 265
x = x ^ (x >> 14);
x = (x + (x << 2)) + (x << 4); // x * 21
x = x ^ (x >> 28);
x = x + (x << 31);
return x;
}
bool keep_node(const nid_t& node_id, const float f) {
// hash the node_id and check if it's accepted given our sparsification factor
return node_hash(node_id) < std::numeric_limits<uint64_t>::max() * f;
}
// Define a struct to hold the coordinates for simplicity
struct Coordinates {
double x1, y1, x2, y2;
};

// Function to adjust node length
Coordinates adjustNodeLength(double x1, double y1, double x2, double y2, double scale, double x_off, double y_off, double sparsification_factor) {
// Apply scale and offsets to original coordinates
x1 = (x1 * scale) - x_off;
y1 = (y1 * scale) + y_off;
x2 = (x2 * scale) - x_off;
y2 = (y2 * scale) + y_off;

// Calculate the original length
double length = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));

// Adjust length based on 1.0 / sparsification_factor
double new_length = sparsification_factor == 0 ? length : length * (1.0 / sparsification_factor);

// Calculate the midpoint
double mid_x = (x1 + x2) / 2.0;
double mid_y = (y1 + y2) / 2.0;

// Calculate the unit vector for the direction
double unit_x = (x2 - x1) / length;
double unit_y = (y2 - y1) / length;

// Calculate new endpoints using the new length
double half_new_length = new_length / 2.0;
double new_x1 = mid_x - half_new_length * unit_x;
double new_y1 = mid_y - half_new_length * unit_y;
double new_x2 = mid_x + half_new_length * unit_x;
double new_y2 = mid_y + half_new_length * unit_y;

// Return the new coordinates
return Coordinates{new_x1, new_y1, new_x2, new_y2};
}
Coordinates adjustNodeEndpoints(const handle_t& handle, const std::vector<double>& X, const std::vector<double>& Y, double scale, double x_off, double y_off, double sparsification_factor, bool lengthen_left_nodes) {
// Original coordinates
uint64_t a = 2 * number_bool_packing::unpack_number(handle);
double x1 = (X[a] * scale) - x_off;
double y1 = (Y[a] * scale) + y_off;
double x2 = (X[a + 1] * scale) - x_off;
double y2 = (Y[a + 1] * scale) + y_off;

// Calculate the original length
double length = std::sqrt(std::pow(x2 - x1, 2) + std::pow(y2 - y1, 2));

// Adjust length based on 1.0 / sparsification_factor
double new_length = !lengthen_left_nodes || sparsification_factor == 0 ? length : length * (1.0 / sparsification_factor);

// Calculate the midpoint
double mid_x = (x1 + x2) / 2.0;
double mid_y = (y1 + y2) / 2.0;

// Calculate the unit vector for the direction
double unit_x = (x2 - x1) / length;
double unit_y = (y2 - y1) / length;

// Calculate new endpoints using the new length
double half_new_length = new_length / 2.0;
double new_x1 = mid_x - half_new_length * unit_x;
double new_y1 = mid_y - half_new_length * unit_y;
double new_x2 = mid_x + half_new_length * unit_x;
double new_y2 = mid_y + half_new_length * unit_y;

return Coordinates{new_x1, new_y1, new_x2, new_y2};
}

void draw_svg(std::ostream &out,
const std::vector<double> &X,
const std::vector<double> &Y,
Expand All @@ -121,7 +206,9 @@ void draw_svg(std::ostream &out,
const double& border,
const double& line_width,
std::vector<algorithms::color_t>& node_id_to_color,
ska::flat_hash_map<handlegraph::nid_t, std::set<std::string>>& node_id_to_label_map) {
ska::flat_hash_map<handlegraph::nid_t, std::set<std::string>>& node_id_to_label_map,
const float& sparsification_factor,
const bool& lengthen_left_nodes) {

std::vector<std::vector<handle_t>> weak_components;
coord_range_2d_t rendered_range;
Expand Down Expand Up @@ -159,42 +246,46 @@ void draw_svg(std::ostream &out,
std::vector<handle_t> highlights;

for (auto& handle : component) {
uint64_t a = 2 * number_bool_packing::unpack_number(handle);
algorithms::color_t color = node_id_to_color.empty() ? COLOR_BLACK : node_id_to_color[graph.get_id(handle)];

if (!(sparsification_factor == 0 || keep_node(graph.get_id(handle), sparsification_factor) || node_id_to_label_map.count(graph.get_id(handle)))) {
continue; // Skip this node to output a lighter SVG (do not nodes with labels, if any)
}

Coordinates newEndpoints = adjustNodeEndpoints(handle, X, Y, scale, x_off, y_off, sparsification_factor, lengthen_left_nodes);

if (color == COLOR_BLACK || color == COLOR_LIGHTGRAY) {
out << "<line x1=\""
<< (X[a] * scale) - x_off
<< newEndpoints.x1
<< "\" x2=\""
<< (X[a + 1] * scale) - x_off
<< newEndpoints.x2
<< "\" y1=\""
<< (Y[a] * scale) + y_off
<< newEndpoints.y1
<< "\" y2=\""
<< (Y[a + 1] * scale) + y_off
<< "\" stroke=\"" << to_hexrgb(color) //to_rgba(color) // with rgb, nodes are invisible with InkScape
<< newEndpoints.y2
<< "\" stroke=\"" << to_hexrgb(color)
<< "\" stroke-width=\"" << line_width
<< "\"/>"
<< std::endl;
} else {
highlights.push_back(handle);
}

double x = (X[a] * scale) - x_off;
double y = (Y[a] * scale) + y_off;
// Check if this is a node with a label
if (node_id_to_label_map.count(graph.get_id(handle))){
// Collect the labels that can be put without overlapping identical ones
std::vector<std::string> labels;
for (auto text : node_id_to_label_map[graph.get_id(handle)]){
if (!is_too_close(x, y, text, 30.0, placed_labels)) {
if (!is_too_close(newEndpoints.x2, newEndpoints.y2, text, 30.0, placed_labels)) {
labels.push_back(text);
}
}
// Check if there is something to label
if (!labels.empty()){
out << "<text font-family=\"Arial\" font-size=\"20\" fill=\"#000000\" stroke=\"#000000\" y=\"" << y << "\">";
out << "<text font-family=\"Arial\" font-size=\"20\" fill=\"#000000\" stroke=\"#000000\" y=\"" << newEndpoints.y2 << "\">";
for (auto text : labels){
out << "<tspan x=\"" << x << "\" dy=\"1.0em\">" << text << "</tspan>";
placed_labels.emplace_back(x, y, text); // Record the label's placement
out << "<tspan x=\"" << newEndpoints.x2 << "\" dy=\"1.0em\">" << text << "</tspan>";
placed_labels.emplace_back(newEndpoints.x2, newEndpoints.y2, text); // Record the label's placement
}
out << "</text>"
<< std::endl;
Expand All @@ -204,17 +295,17 @@ void draw_svg(std::ostream &out,

// color highlights
for (auto& handle : highlights) {
uint64_t a = 2 * number_bool_packing::unpack_number(handle);
Coordinates newEndpoints = adjustNodeEndpoints(handle, X, Y, scale, x_off, y_off, sparsification_factor, lengthen_left_nodes);
algorithms::color_t color = node_id_to_color.empty() ? COLOR_BLACK : node_id_to_color[graph.get_id(handle)];
out << "<line x1=\""
<< (X[a] * scale) - x_off
<< newEndpoints.x1
<< "\" x2=\""
<< (X[a + 1] * scale) - x_off
<< newEndpoints.x2
<< "\" y1=\""
<< (Y[a] * scale) + y_off
<< newEndpoints.y1
<< "\" y2=\""
<< (Y[a + 1] * scale) + y_off
<< "\" stroke=\"" << to_hexrgb(color) //to_rgba(color) // with rgb, nodes are invisible with InkScape
<< newEndpoints.y2
<< "\" stroke=\"" << to_hexrgb(color) //to_rgba(color) // with rgba, nodes are invisible with InkScape
<< "\" stroke-width=\"" << line_width
<< "\"/>"
<< std::endl;
Expand Down
4 changes: 3 additions & 1 deletion src/algorithms/draw.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ void draw_svg(std::ostream &out,
const double& border,
const double& line_width,
std::vector<algorithms::color_t>& node_id_to_color,
ska::flat_hash_map<handlegraph::nid_t, std::set<std::string>>& node_id_to_label_map);
ska::flat_hash_map<handlegraph::nid_t, std::set<std::string>>& node_id_to_label_map,
const float& sparsification_factor,
const bool& lengthen_left_nodes);

std::vector<uint8_t> rasterize(const std::vector<double> &X,
const std::vector<double> &Y,
Expand Down
14 changes: 11 additions & 3 deletions src/subcommand/draw_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ int main_draw(int argc, char **argv) {
"Colors are derived from the 4th column, if present, else from the path name."
"If the 4th column value is in the format 'string#RRGGBB', the RRGGBB color (in hex notation) will be used.",
{'b', "bed-file"});
args::ValueFlag<float> node_sparsification(visualizations_opts, "N", "Remove this fraction of nodes from the SVG output (to output smaller files) (default: 0.0, keep all nodes).", {'f', "svg-sparse-factor"});
args::Flag lengthen_left_nodes(visualizations_opts, "lengthen", "When node sparsitication is active, lengthen the remaining nodes proportionally with the sparsification factor", {'l', "svg-lengthen-nodes"});
args::Group threading(parser, "[ Threading ]");
args::ValueFlag<uint64_t> nthreads(threading, "N", "Number of threads to use for parallel operations.", {'t', "threads"});
args::Group processing_info_opts(parser, "[ Processing Information ]");
Expand Down Expand Up @@ -94,6 +96,12 @@ int main_draw(int argc, char **argv) {
return 1;
}

const float sparse_nodes = node_sparsification ? args::get(node_sparsification) : 0.0;
if (sparse_nodes < 0.0 || sparse_nodes > 1.0) {
std::cerr << "[odgi::draw] error: -f/--svg-sparse-factor must be in the range [0.0, 1.0]." << std::endl;
return 1;
}

const uint64_t num_threads = args::get(nthreads) ? args::get(nthreads) : 1;

graph_t graph;
Expand All @@ -104,7 +112,7 @@ int main_draw(int argc, char **argv) {
if (infile == "-") {
graph.deserialize(std::cin);
} else {
utils::handle_gfa_odgi_input(infile, "draw", args::get(progress), num_threads, graph);
utils::handle_gfa_odgi_input(infile, "draw", lengthen_left_nodes, num_threads, graph);
}
}
}
Expand Down Expand Up @@ -182,7 +190,6 @@ int main_draw(int argc, char **argv) {
const double _png_line_width = png_line_width ? args::get(png_line_width) : 10.0;
const bool _color_paths = args::get(color_paths);
const double _png_path_line_spacing = png_path_line_spacing ? args::get(png_path_line_spacing) : 0.0;
const double svg_scale = !svg_render_scale ? 0.01 : args::get(svg_render_scale);
size_t max_node_depth = 0;
graph.for_each_handle(
[&](const handle_t& h) {
Expand Down Expand Up @@ -219,12 +226,13 @@ int main_draw(int argc, char **argv) {
}

if (svg_out_file) {
const double svg_scale = !svg_render_scale ? 0.01 : args::get(svg_render_scale);
auto& outfile = args::get(svg_out_file);
ofstream f(outfile.c_str());
// todo could be done with callbacks
std::vector<double> X = layout.get_X();
std::vector<double> Y = layout.get_Y();
algorithms::draw_svg(f, X, Y, graph, svg_scale, border_bp, _png_line_width, node_id_to_color, node_id_to_label_map);
algorithms::draw_svg(f, X, Y, graph, svg_scale, border_bp, _png_line_width, node_id_to_color, node_id_to_label_map, sparse_nodes, args::get(lengthen_left_nodes));
f.close();
}

Expand Down

0 comments on commit ff88bfc

Please sign in to comment.