Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

odgi draw: add node sparsification factor for SVG output #558

Merged
merged 4 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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