mirror of
https://github.com/rdkit/rdkit.git
synced 2026-06-03 21:44:30 +08:00
* Configurable legend position (Top/Left/Right/Bottom) and vertical text (GitHub #9023) - Add LegendPosition enum and legendPosition, legendVerticalText to MolDrawOptions - Support legend at Top, Left, Right, Bottom; vertical text for Left/Right - Python: MolDrawOptions.legendPosition, .legendVerticalText; LegendPosition enum - Python: MolToSVG() wrapper with legend/drawOptions; doc updates for MolToImage - JSON: legendPosition (string), legendVerticalText (bool) in draw options - C++ and Python tests; release note and Cartridge.md docs * MolDraw2D: legend gutter for horizontal side legends; vertical side height fit - Reserve horizontal gap between molecule and left/right horizontal legends (scale mol to molWidth-gutter, align toward legend strip). - Position horizontal side legend by measured text width from partition edge. - Vertical side legends: iterative scale so n*max_h+(n-1)*gap fits panel. - Catch: long vertical side legend section. * Update legend-position tests and review-driven cleanup Use enum/default wording for legendPosition docs, move the lightweight Python test to Wrap, add regex-based placement checks (including horizontal side and vertical stacking), and refactor extractLegend helpers per style guidance. * Fix MolDraw2D legend edge cases * MolDraw2D: review follow-up (legend tests, bounds, DRY Top/Bottom) * Update no-FT legend test coords * Address PR review: document constants, remove release-note text, and simplify extra-padding logic
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <Geometry/Transform2D.h>
|
||||
#include <Geometry/Transform3D.h>
|
||||
@@ -1115,8 +1116,24 @@ void DrawMol::calculateScale() {
|
||||
partitionForLegend();
|
||||
|
||||
if (xRange_ > 1e-4 || yRange_ > 1e-4) {
|
||||
double widthForScale = molWidth_ > 0 ? double(molWidth_) : double(drawWidth_);
|
||||
const bool sideLegendHoriz =
|
||||
!legend_.empty() &&
|
||||
(drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Left ||
|
||||
drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Right) &&
|
||||
!drawOptions_.legendVerticalText;
|
||||
if (sideLegendHoriz && molWidth_ > 0) {
|
||||
// Keep a minimum absolute side gap of 8 px so labels do not crowd the
|
||||
// horizontal side legend on small canvases.
|
||||
const double g =
|
||||
std::max(8.0, marginPadding_ * static_cast<double>(drawWidth_));
|
||||
// Require at least ~24 px of drawable molecule width after the gap.
|
||||
if (double(molWidth_) > g + 24.0) {
|
||||
widthForScale = double(molWidth_) - g;
|
||||
}
|
||||
}
|
||||
newScale =
|
||||
std::min(double(drawWidth_) / xRange_, double(molHeight_) / yRange_);
|
||||
std::min(widthForScale / xRange_, double(molHeight_) / yRange_);
|
||||
double fix_scale = newScale;
|
||||
// after all that, use the fixed scale unless it's too big, in which case
|
||||
// scale the drawing down to fit.
|
||||
@@ -1125,7 +1142,7 @@ void DrawMol::calculateScale() {
|
||||
fix_scale = drawOptions_.fixedBondLength;
|
||||
}
|
||||
if (drawOptions_.fixedScale > 0.0) {
|
||||
fix_scale = double(drawWidth_) * drawOptions_.fixedScale;
|
||||
fix_scale = widthForScale * drawOptions_.fixedScale;
|
||||
}
|
||||
if (newScale > fix_scale) {
|
||||
newScale = fix_scale;
|
||||
@@ -1707,20 +1724,250 @@ void DrawMol::calcMeanBondLength() {
|
||||
|
||||
// ****************************************************************************
|
||||
void DrawMol::partitionForLegend() {
|
||||
legendWidth_ = 0;
|
||||
molWidth_ = drawWidth_;
|
||||
if (legend_.empty()) {
|
||||
molHeight_ = drawHeight_;
|
||||
legendHeight_ = 0;
|
||||
} else {
|
||||
if (!flexiCanvasY_) {
|
||||
legendHeight_ = int(drawOptions_.legendFraction * float(drawHeight_));
|
||||
molHeight_ = drawHeight_ - legendHeight_;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
switch (drawOptions_.legendPosition) {
|
||||
case MolDrawOptions::LegendPosition::Bottom:
|
||||
case MolDrawOptions::LegendPosition::Top:
|
||||
if (!flexiCanvasY_) {
|
||||
legendHeight_ = int(drawOptions_.legendFraction * float(drawHeight_));
|
||||
molHeight_ = drawHeight_ - legendHeight_;
|
||||
} else {
|
||||
molHeight_ = drawHeight_;
|
||||
}
|
||||
break;
|
||||
case MolDrawOptions::LegendPosition::Left:
|
||||
case MolDrawOptions::LegendPosition::Right:
|
||||
legendWidth_ = int(drawOptions_.legendFraction * float(drawWidth_));
|
||||
if (legendWidth_ <= 0 && drawWidth_ > 0) {
|
||||
legendWidth_ = 1;
|
||||
}
|
||||
molWidth_ = drawWidth_ - legendWidth_;
|
||||
if (molWidth_ < 1) {
|
||||
molWidth_ = drawWidth_;
|
||||
legendWidth_ = 0;
|
||||
}
|
||||
molHeight_ = drawHeight_;
|
||||
// the legendHeight_ isn't needed for the flexiCanvas
|
||||
legendHeight_ = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void calc_legend_dims(const std::vector<std::string> &legend_bits,
|
||||
double relFontScale, const DrawColour &legendColour,
|
||||
DrawText &textDrawer, double &total_width,
|
||||
double &total_height) {
|
||||
total_width = total_height = 0.0;
|
||||
for (const auto &bit : legend_bits) {
|
||||
double height, width;
|
||||
DrawAnnotation da(bit, TextAlignType::MIDDLE, "legend", relFontScale,
|
||||
Point2D(0.0, 0.0), legendColour, textDrawer);
|
||||
da.getDimensions(width, height);
|
||||
total_height += height;
|
||||
total_width = std::max(total_width, width);
|
||||
}
|
||||
}
|
||||
|
||||
double calc_line_gap(const DrawText &textDrawer, bool vertText,
|
||||
double relFontScale) {
|
||||
const double font_px = textDrawer.fontSize() * relFontScale;
|
||||
if (vertText) {
|
||||
// Vertical legend text needs more inter-glyph breathing room.
|
||||
// 0.35*font_px was tuned to avoid glyph overlap across common fonts.
|
||||
return std::max(1.0, font_px * 0.35);
|
||||
}
|
||||
// Horizontal legend lines can be tighter; keep a 2 px minimum so short
|
||||
// legends remain readable on small canvases.
|
||||
return std::max(2.0, font_px * 0.15);
|
||||
}
|
||||
|
||||
std::vector<std::string> split_legend_bits(const std::string &legend,
|
||||
bool vertText) {
|
||||
std::vector<std::string> legend_bits;
|
||||
if (vertText) {
|
||||
legend_bits.reserve(legend.size());
|
||||
for (char c : legend) {
|
||||
if (c != '\n') {
|
||||
legend_bits.emplace_back(1, c);
|
||||
}
|
||||
}
|
||||
return legend_bits;
|
||||
}
|
||||
boost::split(legend_bits, legend, boost::is_any_of("\n"));
|
||||
legend_bits.erase(std::remove_if(legend_bits.begin(), legend_bits.end(),
|
||||
[](const std::string &s) {
|
||||
return s.empty();
|
||||
}),
|
||||
legend_bits.end());
|
||||
return legend_bits;
|
||||
}
|
||||
|
||||
void place_bottom_legend(const std::vector<std::string> &legend_bits,
|
||||
double relFontScale, const DrawColour &legendColour,
|
||||
DrawText &textDrawer, double baseX, double baseY,
|
||||
double drawWidth, double drawHeight,
|
||||
std::vector<std::unique_ptr<DrawAnnotation>> &legends) {
|
||||
Point2D loc(baseX + drawWidth / 2.0, baseY + drawHeight);
|
||||
for (const auto &bit : legend_bits) {
|
||||
legends.emplace_back(new DrawAnnotation(bit, TextAlignType::MIDDLE, "legend",
|
||||
relFontScale, loc, legendColour,
|
||||
textDrawer));
|
||||
}
|
||||
double xmin, xmax, ymin, ymax;
|
||||
double lastAbove = 0.0;
|
||||
for (int i = static_cast<int>(legends.size()) - 1; i >= 0; --i) {
|
||||
xmin = ymin = std::numeric_limits<double>::max();
|
||||
xmax = ymax = std::numeric_limits<double>::lowest();
|
||||
legends[i]->findExtremes(xmin, xmax, ymin, ymax);
|
||||
double thisBelow = legends[i]->pos_.y - ymax;
|
||||
double thisAbove = legends[i]->pos_.y - ymin;
|
||||
if (i == static_cast<int>(legends.size()) - 1) {
|
||||
legends[i]->pos_.y += thisBelow;
|
||||
} else {
|
||||
legends[i]->pos_.y = legends[i + 1]->pos_.y + thisBelow - lastAbove;
|
||||
}
|
||||
lastAbove = thisAbove;
|
||||
}
|
||||
}
|
||||
|
||||
void place_top_legend(const std::vector<std::string> &legend_bits,
|
||||
double relFontScale, const DrawColour &legendColour,
|
||||
DrawText &textDrawer, double baseX, double baseY,
|
||||
double drawWidth, double drawHeight, int legendHeight,
|
||||
std::vector<std::unique_ptr<DrawAnnotation>> &legends) {
|
||||
const double gap = calc_line_gap(textDrawer, false, relFontScale);
|
||||
double total_h = 0.0;
|
||||
for (size_t i = 0; i < legend_bits.size(); ++i) {
|
||||
double height, width;
|
||||
DrawAnnotation da(legend_bits[i], TextAlignType::MIDDLE, "legend",
|
||||
relFontScale, Point2D(0.0, 0.0), legendColour, textDrawer);
|
||||
da.getDimensions(width, height);
|
||||
total_h += height;
|
||||
if (i + 1 < legend_bits.size()) {
|
||||
total_h += gap;
|
||||
}
|
||||
}
|
||||
const double band_h = legendHeight > 0 ? double(legendHeight) : drawHeight;
|
||||
double y = baseY + (band_h - total_h) / 2.0;
|
||||
for (size_t i = 0; i < legend_bits.size(); ++i) {
|
||||
double height, width;
|
||||
DrawAnnotation da(legend_bits[i], TextAlignType::MIDDLE, "legend",
|
||||
relFontScale, Point2D(0.0, 0.0), legendColour, textDrawer);
|
||||
da.getDimensions(width, height);
|
||||
y += height / 2.0;
|
||||
Point2D loc(baseX + drawWidth / 2.0, y);
|
||||
legends.emplace_back(new DrawAnnotation(legend_bits[i], TextAlignType::MIDDLE,
|
||||
"legend", relFontScale, loc,
|
||||
legendColour, textDrawer));
|
||||
y += height / 2.0;
|
||||
if (i + 1 < legend_bits.size()) {
|
||||
y += gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void place_side_legend(const std::vector<std::string> &legend_bits,
|
||||
double relFontScale, const DrawColour &legendColour,
|
||||
DrawText &textDrawer, bool vertText, bool is_left,
|
||||
double baseX, double baseY, int legendWidth, int molWidth,
|
||||
double drawWidth, double drawHeight, double marginPadding,
|
||||
std::vector<std::unique_ptr<DrawAnnotation>> &legends) {
|
||||
if (legend_bits.empty()) {
|
||||
return;
|
||||
}
|
||||
double stripCentreX = 0.0;
|
||||
if (vertText) {
|
||||
stripCentreX = is_left ? baseX + legendWidth / 2.0
|
||||
: baseX + drawWidth - legendWidth / 2.0;
|
||||
}
|
||||
const double stripCentreY = baseY + drawHeight / 2.0;
|
||||
const double gap = calc_line_gap(textDrawer, vertText, relFontScale);
|
||||
if (!vertText) {
|
||||
double maxLineW = 0.0;
|
||||
for (const auto &bit : legend_bits) {
|
||||
double h = 0.0, w = 0.0;
|
||||
DrawAnnotation da(bit, TextAlignType::MIDDLE, "legend", relFontScale,
|
||||
Point2D(0.0, 0.0), legendColour, textDrawer);
|
||||
da.getDimensions(w, h);
|
||||
maxLineW = std::max(maxLineW, w);
|
||||
}
|
||||
const double pad = std::max(8.0, marginPadding * drawWidth);
|
||||
if (is_left) {
|
||||
const double molPanelLeft = baseX + static_cast<double>(legendWidth);
|
||||
stripCentreX = molPanelLeft - pad - maxLineW / 2.0;
|
||||
stripCentreX = std::max(stripCentreX, baseX + maxLineW / 2.0 + 2.0);
|
||||
} else {
|
||||
const double molPanelRight = baseX + static_cast<double>(molWidth);
|
||||
stripCentreX = molPanelRight + pad + maxLineW / 2.0;
|
||||
stripCentreX =
|
||||
std::min(stripCentreX, baseX + drawWidth - maxLineW / 2.0 - 2.0);
|
||||
}
|
||||
}
|
||||
if (vertText) {
|
||||
double max_h = 0.0;
|
||||
for (const auto &bit : legend_bits) {
|
||||
double height, width;
|
||||
DrawAnnotation da(bit, TextAlignType::MIDDLE, "legend", relFontScale,
|
||||
Point2D(0.0, 0.0), legendColour, textDrawer);
|
||||
da.getDimensions(width, height);
|
||||
max_h = std::max(max_h, height);
|
||||
}
|
||||
const double total_h =
|
||||
legend_bits.size() * max_h + (legend_bits.size() - 1) * gap;
|
||||
double y = stripCentreY - total_h / 2.0;
|
||||
for (size_t i = 0; i < legend_bits.size(); ++i) {
|
||||
y += max_h / 2.0;
|
||||
Point2D loc(stripCentreX, y);
|
||||
legends.emplace_back(new DrawAnnotation(legend_bits[i], TextAlignType::MIDDLE,
|
||||
"legend", relFontScale, loc,
|
||||
legendColour, textDrawer));
|
||||
y += max_h / 2.0;
|
||||
if (i + 1 < legend_bits.size()) {
|
||||
y += gap;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
double total_h = 0.0;
|
||||
for (size_t i = 0; i < legend_bits.size(); ++i) {
|
||||
double height, width;
|
||||
DrawAnnotation da(legend_bits[i], TextAlignType::MIDDLE, "legend",
|
||||
relFontScale, Point2D(0.0, 0.0), legendColour,
|
||||
textDrawer);
|
||||
da.getDimensions(width, height);
|
||||
total_h += height;
|
||||
if (i + 1 < legend_bits.size()) {
|
||||
total_h += gap;
|
||||
}
|
||||
}
|
||||
double y = stripCentreY - total_h / 2.0;
|
||||
for (size_t i = 0; i < legend_bits.size(); ++i) {
|
||||
double height, width;
|
||||
DrawAnnotation da(legend_bits[i], TextAlignType::MIDDLE, "legend",
|
||||
relFontScale, Point2D(0.0, 0.0), legendColour,
|
||||
textDrawer);
|
||||
da.getDimensions(width, height);
|
||||
y += height / 2.0;
|
||||
Point2D loc(stripCentreX, y);
|
||||
legends.emplace_back(new DrawAnnotation(legend_bits[i], TextAlignType::MIDDLE,
|
||||
"legend", relFontScale, loc,
|
||||
legendColour, textDrawer));
|
||||
y += height / 2.0;
|
||||
if (i + 1 < legend_bits.size()) {
|
||||
y += gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ****************************************************************************
|
||||
// This must be called after calculateScale() because it needs the final
|
||||
// font size to work out the legend font size which is given in
|
||||
@@ -1730,97 +1977,131 @@ void DrawMol::extractLegend() {
|
||||
if (legend_.empty()) {
|
||||
return;
|
||||
}
|
||||
auto calc_legend_height = [&](const std::vector<std::string> &legend_bits,
|
||||
double relFontScale, double &total_width,
|
||||
double &total_height) {
|
||||
total_width = total_height = 0;
|
||||
for (auto &bit : legend_bits) {
|
||||
double height, width;
|
||||
DrawAnnotation da(bit, TextAlignType::MIDDLE, "legend", relFontScale,
|
||||
Point2D(0.0, 0.0), drawOptions_.legendColour,
|
||||
textDrawer_);
|
||||
da.getDimensions(width, height);
|
||||
total_height += height;
|
||||
total_width = std::max(total_width, width);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::string> legend_bits;
|
||||
// split any strings on newlines
|
||||
std::string next_piece;
|
||||
for (auto c : legend_) {
|
||||
if (c == '\n') {
|
||||
if (!next_piece.empty()) {
|
||||
legend_bits.push_back(next_piece);
|
||||
}
|
||||
next_piece = "";
|
||||
} else {
|
||||
next_piece += c;
|
||||
}
|
||||
}
|
||||
if (!next_piece.empty()) {
|
||||
legend_bits.push_back(next_piece);
|
||||
const bool vertText =
|
||||
(drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Left ||
|
||||
drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Right) &&
|
||||
drawOptions_.legendVerticalText;
|
||||
std::vector<std::string> legend_bits = split_legend_bits(legend_, vertText);
|
||||
if (legend_bits.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// work out a font scale that allows the pieces to fit, remembering there's
|
||||
// padding round the picture.
|
||||
double fsize = textDrawer_.fontSize();
|
||||
double relFontScale = drawOptions_.legendFontSize / fsize;
|
||||
double total_width, total_height;
|
||||
calc_legend_height(legend_bits, relFontScale, total_width, total_height);
|
||||
if (total_width >= drawWidth_) {
|
||||
if (!flexiCanvasX_) {
|
||||
relFontScale *= double(drawWidth_) / total_width;
|
||||
calc_legend_height(legend_bits, relFontScale, total_width, total_height);
|
||||
} else {
|
||||
width_ = total_width * (1 + 2 * marginPadding_);
|
||||
drawWidth_ = total_width;
|
||||
}
|
||||
}
|
||||
calc_legend_dims(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, total_width, total_height);
|
||||
|
||||
if (!flexiCanvasY_) {
|
||||
auto adjLegHt = drawHeight_ * drawOptions_.legendFraction;
|
||||
// subtract off space for the padding.
|
||||
if (total_height > adjLegHt) {
|
||||
relFontScale *= double(adjLegHt) / total_height;
|
||||
calc_legend_height(legend_bits, relFontScale, total_width, total_height);
|
||||
}
|
||||
double maxWidth = double(drawWidth_);
|
||||
double maxHeight = double(drawHeight_);
|
||||
if (drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Left ||
|
||||
drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Right) {
|
||||
maxWidth = legendWidth_ > 0 ? double(legendWidth_) : double(drawWidth_);
|
||||
} else {
|
||||
// a small gap between the legend and the picture looks better,
|
||||
// and make it at least 2 pixels.
|
||||
double extra_padding = total_height * marginPadding_;
|
||||
extra_padding = extra_padding < 2.0 ? 2.0 : extra_padding;
|
||||
legendHeight_ = total_height + extra_padding;
|
||||
drawHeight_ += legendHeight_;
|
||||
height_ += legendHeight_;
|
||||
maxHeight = legendHeight_ > 0 ? double(legendHeight_) : double(drawHeight_);
|
||||
}
|
||||
|
||||
Point2D loc(drawWidth_ / 2 + xOffset_ + width_ * marginPadding_,
|
||||
marginPadding_ * height_ + drawHeight_ + yOffset_);
|
||||
for (auto bit : legend_bits) {
|
||||
DrawAnnotation *da =
|
||||
new DrawAnnotation(bit, TextAlignType::MIDDLE, "legend", relFontScale,
|
||||
loc, drawOptions_.legendColour, textDrawer_);
|
||||
legends_.emplace_back(da);
|
||||
// For horizontal text on the sides (Left/Right, legendVerticalText false),
|
||||
// do not scale down by strip width so the font size matches Top/Bottom.
|
||||
const bool sideHorizontal =
|
||||
(drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Left ||
|
||||
drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Right) &&
|
||||
!vertText;
|
||||
if (total_width > maxWidth && maxWidth > 0 && !flexiCanvasX_ && !sideHorizontal) {
|
||||
relFontScale *= maxWidth / total_width;
|
||||
calc_legend_dims(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, total_width, total_height);
|
||||
}
|
||||
// The letters have different amounts above and below the centre,
|
||||
// which matters when placing them vertically.
|
||||
// Draw them from the bottom up.
|
||||
double xmin, xmax, ymin, ymax;
|
||||
xmin = ymin = std::numeric_limits<double>::max();
|
||||
xmax = ymax = std::numeric_limits<double>::lowest();
|
||||
legends_.back()->findExtremes(xmin, xmax, ymin, ymax);
|
||||
double lastBelow = legends_.back()->pos_.y - ymax;
|
||||
double lastAbove = legends_.back()->pos_.y - ymin;
|
||||
legends_.back()->pos_.y += lastBelow;
|
||||
for (int i = legends_.size() - 2; i >= 0; --i) {
|
||||
xmin = ymin = std::numeric_limits<double>::max();
|
||||
xmax = ymax = std::numeric_limits<double>::lowest();
|
||||
legends_[i]->findExtremes(xmin, xmax, ymin, ymax);
|
||||
double thisBelow = legends_[i]->pos_.y - ymax;
|
||||
double thisAbove = legends_[i]->pos_.y - ymin;
|
||||
legends_[i]->pos_.y = legends_[i + 1]->pos_.y + thisBelow - lastAbove;
|
||||
lastAbove = thisAbove;
|
||||
if (total_height > maxHeight && maxHeight > 0) {
|
||||
if (!flexiCanvasY_) {
|
||||
relFontScale *= maxHeight / total_height;
|
||||
calc_legend_dims(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, total_width, total_height);
|
||||
} else if (drawOptions_.legendPosition ==
|
||||
MolDrawOptions::LegendPosition::Bottom ||
|
||||
drawOptions_.legendPosition ==
|
||||
MolDrawOptions::LegendPosition::Top) {
|
||||
double extra_padding = total_height * marginPadding_;
|
||||
extra_padding = std::max(extra_padding, 2.0);
|
||||
legendHeight_ = int(total_height + extra_padding);
|
||||
drawHeight_ += legendHeight_;
|
||||
height_ += legendHeight_;
|
||||
if (drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Top) {
|
||||
molHeight_ = drawHeight_ - legendHeight_;
|
||||
}
|
||||
calc_legend_dims(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, total_width, total_height);
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal text on the sides should match Top/Bottom size: never shrink
|
||||
// below the default legend font scale (canvas-sized scaling made it tiny).
|
||||
const double initialFontScale = drawOptions_.legendFontSize / fsize;
|
||||
if (sideHorizontal && relFontScale < initialFontScale) {
|
||||
relFontScale = initialFontScale;
|
||||
calc_legend_dims(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, total_width, total_height);
|
||||
}
|
||||
|
||||
// Vertical side legends use uniform line height + gaps; scale so the stack
|
||||
// fits the molecule panel (sum of per-glyph heights can underestimate span).
|
||||
if (vertText &&
|
||||
(drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Left ||
|
||||
drawOptions_.legendPosition == MolDrawOptions::LegendPosition::Right) &&
|
||||
!legend_bits.empty() && drawHeight_ > 0) {
|
||||
const double marginY =
|
||||
std::max(4.0, marginPadding_ * static_cast<double>(height_));
|
||||
const double avail = std::max(1.0, drawHeight_ - 2.0 * marginY);
|
||||
for (int iter = 0; iter < 8; ++iter) {
|
||||
double max_h = 0.0;
|
||||
for (const auto &bit : legend_bits) {
|
||||
double h = 0.0, w = 0.0;
|
||||
DrawAnnotation da(bit, TextAlignType::MIDDLE, "legend", relFontScale,
|
||||
Point2D(0.0, 0.0), drawOptions_.legendColour,
|
||||
textDrawer_);
|
||||
da.getDimensions(w, h);
|
||||
max_h = std::max(max_h, h);
|
||||
}
|
||||
const double gap = calc_line_gap(textDrawer_, vertText, relFontScale);
|
||||
const size_t n = legend_bits.size();
|
||||
const double span =
|
||||
static_cast<double>(n) * max_h +
|
||||
(n > 1 ? static_cast<double>(n - 1) * gap : 0.0);
|
||||
if (span <= avail || span <= 0.0) {
|
||||
break;
|
||||
}
|
||||
relFontScale *= avail / span;
|
||||
calc_legend_dims(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, total_width, total_height);
|
||||
}
|
||||
}
|
||||
|
||||
const double baseX = xOffset_ + width_ * marginPadding_;
|
||||
const double baseY = marginPadding_ * height_ + yOffset_;
|
||||
|
||||
switch (drawOptions_.legendPosition) {
|
||||
case MolDrawOptions::LegendPosition::Bottom:
|
||||
place_bottom_legend(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, baseX, baseY, drawWidth_, drawHeight_,
|
||||
legends_);
|
||||
break;
|
||||
case MolDrawOptions::LegendPosition::Top:
|
||||
place_top_legend(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, baseX, baseY, drawWidth_, drawHeight_,
|
||||
legendHeight_, legends_);
|
||||
break;
|
||||
case MolDrawOptions::LegendPosition::Left:
|
||||
place_side_legend(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, vertText, true, baseX, baseY, legendWidth_,
|
||||
molWidth_, drawWidth_, drawHeight_, marginPadding_,
|
||||
legends_);
|
||||
break;
|
||||
case MolDrawOptions::LegendPosition::Right:
|
||||
place_side_legend(legend_bits, relFontScale, drawOptions_.legendColour,
|
||||
textDrawer_, vertText, false, baseX, baseY, legendWidth_,
|
||||
molWidth_, drawWidth_, drawHeight_, marginPadding_,
|
||||
legends_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2802,10 +3083,43 @@ void DrawMol::getDrawTransformers(Point2D &trans, Point2D &scale,
|
||||
trans = Point2D(-xMin_, -yMin_);
|
||||
scale = Point2D(scale_, scale_);
|
||||
Point2D scaledRanges(scale_ * xRange_, scale_ * yRange_);
|
||||
toCentre = Point2D(
|
||||
(drawWidth_ - scaledRanges.x) / 2.0 + xOffset_ + width_ * marginPadding_,
|
||||
(molHeight_ - scaledRanges.y) / 2.0 + yOffset_ +
|
||||
height_ * marginPadding_);
|
||||
double molW = (molWidth_ > 0) ? double(molWidth_) : double(drawWidth_);
|
||||
double xCentre =
|
||||
(molW - scaledRanges.x) / 2.0 + xOffset_ + width_ * marginPadding_;
|
||||
double yCentre =
|
||||
(molHeight_ - scaledRanges.y) / 2.0 + yOffset_ + height_ * marginPadding_;
|
||||
const double basePanelX = xOffset_ + width_ * marginPadding_;
|
||||
switch (drawOptions_.legendPosition) {
|
||||
case MolDrawOptions::LegendPosition::Bottom:
|
||||
break;
|
||||
case MolDrawOptions::LegendPosition::Top:
|
||||
yCentre += legendHeight_;
|
||||
break;
|
||||
case MolDrawOptions::LegendPosition::Left:
|
||||
if (!legend_.empty() && !drawOptions_.legendVerticalText &&
|
||||
molWidth_ > 0) {
|
||||
// Same minimum side gap used in initDrawMolecule() so scaling and final
|
||||
// placement stay consistent for horizontal side legends.
|
||||
const double g =
|
||||
std::max(8.0, marginPadding_ * static_cast<double>(drawWidth_));
|
||||
// Drawn x spans [toCentre.x, toCentre.x + scaledRanges.x]; leave g past
|
||||
// the legend strip before the molecule.
|
||||
xCentre = basePanelX + double(legendWidth_) + g;
|
||||
} else {
|
||||
xCentre += legendWidth_;
|
||||
}
|
||||
break;
|
||||
case MolDrawOptions::LegendPosition::Right:
|
||||
if (!legend_.empty() && !drawOptions_.legendVerticalText &&
|
||||
molWidth_ > 0) {
|
||||
// Same minimum side gap used on the left side for symmetry.
|
||||
const double g =
|
||||
std::max(8.0, marginPadding_ * static_cast<double>(drawWidth_));
|
||||
xCentre = basePanelX + double(molWidth_) - g - scaledRanges.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
toCentre = Point2D(xCentre, yCentre);
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
|
||||
@@ -306,8 +306,11 @@ class DrawMol {
|
||||
double meanBondLength_ = 0.0;
|
||||
// if there's a legend, we reserve a bit for it. molHeight_ is the
|
||||
// bit for drawing the molecule, legendHeight_ the bit under that
|
||||
// for the legend. In pixels.
|
||||
// for the legend. For Left/Right, legendWidth_ is the side strip.
|
||||
// molWidth_ is the width available for the molecule (drawWidth_ for
|
||||
// Bottom/Top, drawWidth_ - legendWidth_ for Left/Right). In pixels.
|
||||
int molHeight_, legendHeight_ = 0;
|
||||
int molWidth_ = 0, legendWidth_ = 0;
|
||||
bool drawingInitialised_ = false;
|
||||
// when drawing the atoms and bonds in an SVG, they are given a class
|
||||
// via MolDraw2D's activeAtmIdx[12]_ and activeBndIdx. We don't always want
|
||||
|
||||
@@ -215,6 +215,13 @@ struct RDKIT_MOLDRAW2D_EXPORT MolDrawOptions {
|
||||
// BuiltinRobotoRegular.
|
||||
DrawColour legendColour{0, 0,
|
||||
0}; // color to be used for the legend (if present)
|
||||
//! Legend position relative to the molecule (only Bottom supported in
|
||||
//! drawMolecules grid; all four work for single-molecule drawing).
|
||||
enum class LegendPosition { Bottom, Top, Left, Right };
|
||||
LegendPosition legendPosition = LegendPosition::Bottom;
|
||||
//! When legend is Left or Right, draw text vertically (one character per
|
||||
//! line). Ignored for Top/Bottom.
|
||||
bool legendVerticalText = true;
|
||||
double multipleBondOffset = 0.15; // offset for the extra lines
|
||||
// in a multiple bond as a fraction of
|
||||
// mean bond length
|
||||
|
||||
@@ -104,6 +104,26 @@ void get_highlight_style_option(const boost::property_tree::ptree &pt,
|
||||
}
|
||||
}
|
||||
|
||||
void get_legend_position_option(const boost::property_tree::ptree &pt,
|
||||
const char *pnm,
|
||||
MolDrawOptions::LegendPosition &pos) {
|
||||
PRECONDITION(pnm && strlen(pnm), "bad property name");
|
||||
if (pt.find(pnm) == pt.not_found()) {
|
||||
return;
|
||||
}
|
||||
const auto &node = pt.get_child(pnm);
|
||||
auto str = node.get_value<std::string>();
|
||||
if (str == "Top") {
|
||||
pos = MolDrawOptions::LegendPosition::Top;
|
||||
} else if (str == "Left") {
|
||||
pos = MolDrawOptions::LegendPosition::Left;
|
||||
} else if (str == "Right") {
|
||||
pos = MolDrawOptions::LegendPosition::Right;
|
||||
} else {
|
||||
pos = MolDrawOptions::LegendPosition::Bottom;
|
||||
}
|
||||
}
|
||||
|
||||
void updateMolDrawOptionsFromJSON(MolDrawOptions &opts,
|
||||
const std::string &json) {
|
||||
if (json.empty()) {
|
||||
@@ -188,6 +208,8 @@ void updateMolDrawOptionsFromJSON(MolDrawOptions &opts,
|
||||
}
|
||||
get_highlight_style_option(pt, "multiColourHighlightStyle",
|
||||
opts.multiColourHighlightStyle);
|
||||
get_legend_position_option(pt, "legendPosition", opts.legendPosition);
|
||||
PT_OPT_GET(legendVerticalText);
|
||||
const auto drawingExtentsIncludeIt = pt.find("drawingExtentsInclude");
|
||||
if (drawingExtentsIncludeIt != pt.not_found()) {
|
||||
bool haveDrawElementFlags = false;
|
||||
|
||||
@@ -788,6 +788,13 @@ BOOST_PYTHON_MODULE(rdMolDraw2D) {
|
||||
.value("Lasso", RDKit::MultiColourHighlightStyle::LASSO)
|
||||
.export_values();
|
||||
|
||||
python::enum_<RDKit::MolDrawOptions::LegendPosition>("LegendPosition")
|
||||
.value("Bottom", RDKit::MolDrawOptions::LegendPosition::Bottom)
|
||||
.value("Top", RDKit::MolDrawOptions::LegendPosition::Top)
|
||||
.value("Left", RDKit::MolDrawOptions::LegendPosition::Left)
|
||||
.value("Right", RDKit::MolDrawOptions::LegendPosition::Right)
|
||||
.export_values();
|
||||
|
||||
{
|
||||
python::enum_<RDKit::DrawElement::_enumerated> drawElementEnum(
|
||||
"DrawElement");
|
||||
@@ -919,6 +926,14 @@ BOOST_PYTHON_MODULE(rdMolDraw2D) {
|
||||
.def_readwrite(
|
||||
"legendFraction", &RDKit::MolDrawOptions::legendFraction,
|
||||
"fraction of the draw panel to be used for the legend if present")
|
||||
.def_readwrite(
|
||||
"legendPosition", &RDKit::MolDrawOptions::legendPosition,
|
||||
"legend position enum. Default=Bottom. "
|
||||
"Values: LegendPosition.Bottom, LegendPosition.Top, "
|
||||
"LegendPosition.Left, LegendPosition.Right.")
|
||||
.def_readwrite(
|
||||
"legendVerticalText", &RDKit::MolDrawOptions::legendVerticalText,
|
||||
"when legend is Left or Right, draw text vertically (one char per line)")
|
||||
.def_readwrite("maxFontSize", &RDKit::MolDrawOptions::maxFontSize,
|
||||
"maximum font size in pixels. default=40, -1 means no"
|
||||
" maximum.")
|
||||
|
||||
@@ -989,6 +989,15 @@ M END
|
||||
highlightCoords = extractCoords(text)
|
||||
self.assertTrue(checkCoords(referenceCoords, highlightCoords))
|
||||
|
||||
def testLegendPosition(self):
|
||||
mol = Chem.MolFromSmiles('CCO')
|
||||
self.assertIsNotNone(mol)
|
||||
opts = rdMolDraw2D.MolDrawOptions()
|
||||
self.assertEqual(opts.legendPosition, rdMolDraw2D.LegendPosition.Bottom)
|
||||
opts.legendPosition = rdMolDraw2D.LegendPosition.Top
|
||||
svg = Draw.MolToSVG(mol, legend='Ethanol', drawOptions=opts)
|
||||
self.assertIn("class='legend'", svg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -74,9 +74,9 @@ const std::map<std::string, std::hash_result_t> SVG_HASHES = {
|
||||
{"testDativeBonds_2c.svg", 1745138239U},
|
||||
{"testDativeBonds_2d.svg", 3279423301U},
|
||||
{"testZeroOrderBonds_1.svg", 3733430366U},
|
||||
{"testFoundations_1.svg", 2350247048U},
|
||||
{"testFoundations_2.svg", 15997352U},
|
||||
{"testTest_1.svg", 15997352U},
|
||||
{"testFoundations_1.svg", 2283802316U},
|
||||
{"testFoundations_2.svg", 2468031318U},
|
||||
{"testTest_1.svg", 2468031318U},
|
||||
{"testKekulizationProblems_1.svg", 2284161107U},
|
||||
{"testAtomBondIndices_1.svg", 2702803018U},
|
||||
{"testAtomBondIndices_2.svg", 1564350363U},
|
||||
@@ -219,9 +219,9 @@ const std::map<std::string, std::hash_result_t> SVG_HASHES = {
|
||||
{"testGithub4764.sz3.svg", 2712214121U},
|
||||
{"testDrawArc1.svg", 3279637525U},
|
||||
{"testMetalWedges.svg", 2896721486U},
|
||||
{"testVariableLegend_1.svg", 1817838365U},
|
||||
{"testVariableLegend_2.svg", 1038247753U},
|
||||
{"testVariableLegend_3.svg", 2073034956U},
|
||||
{"testVariableLegend_1.svg", 1208675629U},
|
||||
{"testVariableLegend_2.svg", 799897710U},
|
||||
{"testVariableLegend_3.svg", 2599269417U},
|
||||
{"testGithub_5061.svg", 2050932431U},
|
||||
{"testGithub_5185.svg", 3800073130U},
|
||||
{"testGithub_5269_1.svg", 4160868253U},
|
||||
@@ -4855,6 +4855,176 @@ TEST_CASE("vary proportion of panel for legend", "[drawing]") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("legend position Top Left Right and vertical text", "[drawing]") {
|
||||
auto m1 = "CCO"_smiles;
|
||||
REQUIRE(m1);
|
||||
const std::string legend("Ethanol");
|
||||
auto get_legend_xy = [](const std::string &text, double &x, double &y) {
|
||||
std::smatch match;
|
||||
std::regex textRgx(
|
||||
"<text x='(-?[0-9]+\\.?[0-9]*)' y='(-?[0-9]+\\.?[0-9]*)' class='legend'");
|
||||
if (std::regex_search(text, match, textRgx) && match.size() == 3) {
|
||||
x = std::stod(match[1].str());
|
||||
y = std::stod(match[2].str());
|
||||
return true;
|
||||
}
|
||||
std::regex pathRgx("class='legend' d='M (-?[0-9]+\\.?[0-9]*) "
|
||||
"(-?[0-9]+\\.?[0-9]*)");
|
||||
if (std::regex_search(text, match, pathRgx) && match.size() == 3) {
|
||||
x = std::stod(match[1].str());
|
||||
y = std::stod(match[2].str());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
auto get_all_legend_xy = [](const std::string &text) {
|
||||
std::vector<std::pair<double, double>> coords;
|
||||
std::regex textRgx(
|
||||
"<text x='(-?[0-9]+\\.?[0-9]*)' y='(-?[0-9]+\\.?[0-9]*)' class='legend'");
|
||||
for (auto it = std::sregex_iterator(text.begin(), text.end(), textRgx);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
coords.emplace_back(std::stod((*it)[1].str()), std::stod((*it)[2].str()));
|
||||
}
|
||||
if (!coords.empty()) {
|
||||
return coords;
|
||||
}
|
||||
std::regex pathRgx("class='legend' d='M (-?[0-9]+\\.?[0-9]*) "
|
||||
"(-?[0-9]+\\.?[0-9]*)");
|
||||
for (auto it = std::sregex_iterator(text.begin(), text.end(), pathRgx);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
coords.emplace_back(std::stod((*it)[1].str()), std::stod((*it)[2].str()));
|
||||
}
|
||||
return coords;
|
||||
};
|
||||
double top_x = 0.0, top_y = 0.0;
|
||||
double bottom_x = 0.0, bottom_y = 0.0;
|
||||
double left_x = 0.0, left_y = 0.0;
|
||||
double right_x = 0.0, right_y = 0.0;
|
||||
SECTION("Top") {
|
||||
MolDraw2DSVG drawer(200, 200, -1, -1, NO_FREETYPE);
|
||||
drawer.drawOptions().legendPosition =
|
||||
MolDrawOptions::LegendPosition::Top;
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, legend);
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
CHECK(get_legend_xy(text, top_x, top_y));
|
||||
// Legend should sit in the upper part of the canvas (SVG y grows down).
|
||||
CHECK(top_y < 50.0);
|
||||
std::ofstream outs("testLegendPosition_top.svg");
|
||||
outs << text;
|
||||
outs.flush();
|
||||
}
|
||||
SECTION("Left with vertical text") {
|
||||
MolDraw2DSVG drawer(200, 200, -1, -1, NO_FREETYPE);
|
||||
drawer.drawOptions().legendPosition =
|
||||
MolDrawOptions::LegendPosition::Left;
|
||||
drawer.drawOptions().legendVerticalText = true;
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, legend);
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
auto coords = get_all_legend_xy(text);
|
||||
REQUIRE(coords.size() > 1);
|
||||
for (size_t i = 1; i < coords.size(); ++i) {
|
||||
CHECK(coords[i].second > coords[i - 1].second);
|
||||
}
|
||||
std::ofstream outs("testLegendPosition_left_vertical.svg");
|
||||
outs << text;
|
||||
outs.flush();
|
||||
}
|
||||
SECTION("Left horizontal") {
|
||||
MolDraw2DSVG drawer(200, 200, -1, -1, NO_FREETYPE);
|
||||
drawer.drawOptions().legendPosition =
|
||||
MolDrawOptions::LegendPosition::Left;
|
||||
drawer.drawOptions().legendVerticalText = false;
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, legend);
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
CHECK(get_legend_xy(text, left_x, left_y));
|
||||
CHECK(left_x < 80.0);
|
||||
std::ofstream outs("testLegendPosition_left_horizontal.svg");
|
||||
outs << text;
|
||||
outs.flush();
|
||||
}
|
||||
SECTION("Right horizontal") {
|
||||
MolDraw2DSVG drawer(200, 200, -1, -1, NO_FREETYPE);
|
||||
drawer.drawOptions().legendPosition =
|
||||
MolDrawOptions::LegendPosition::Right;
|
||||
drawer.drawOptions().legendVerticalText = false;
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, legend);
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
CHECK(get_legend_xy(text, right_x, right_y));
|
||||
CHECK(right_x > 100.0);
|
||||
std::ofstream outs("testLegendPosition_right_horizontal.svg");
|
||||
outs << text;
|
||||
outs.flush();
|
||||
}
|
||||
SECTION("Bottom unchanged default") {
|
||||
MolDraw2DSVG drawer(200, 200, -1, -1, NO_FREETYPE);
|
||||
CHECK(drawer.drawOptions().legendPosition ==
|
||||
MolDrawOptions::LegendPosition::Bottom);
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, legend);
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
CHECK(get_legend_xy(text, bottom_x, bottom_y));
|
||||
CHECK(bottom_y > 140.0);
|
||||
std::ofstream outs("testLegendPosition_bottom.svg");
|
||||
outs << text;
|
||||
outs.flush();
|
||||
}
|
||||
SECTION("Long vertical side legend fits panel height") {
|
||||
const std::string longName(48, 'M');
|
||||
MolDraw2DSVG drawer(160, 90, -1, -1, NO_FREETYPE);
|
||||
drawer.drawOptions().legendPosition =
|
||||
MolDrawOptions::LegendPosition::Left;
|
||||
drawer.drawOptions().legendVerticalText = true;
|
||||
drawer.drawOptions().legendFraction = 0.22f;
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, longName);
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
// At this size the fitted legend can be very small but should still be
|
||||
// present in the SVG and within the panel.
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
std::ofstream outs("testLegendPosition_long_vertical.svg");
|
||||
outs << text;
|
||||
outs.flush();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("legend options from JSON", "[drawing]") {
|
||||
auto m1 = "CCO"_smiles;
|
||||
REQUIRE(m1);
|
||||
SECTION("legendPosition and legendVerticalText parsed from JSON") {
|
||||
const char *json =
|
||||
R"({"legendPosition": "Top", "legendVerticalText": true})";
|
||||
MolDrawOptions opts;
|
||||
MolDraw2DUtils::updateMolDrawOptionsFromJSON(opts, json);
|
||||
CHECK(opts.legendPosition == MolDrawOptions::LegendPosition::Top);
|
||||
CHECK(opts.legendVerticalText == true);
|
||||
MolDraw2DSVG drawer(200, 200, -1, -1, NO_FREETYPE);
|
||||
drawer.drawOptions() = opts;
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, "Ethanol");
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
}
|
||||
SECTION("legendPosition Left and legendFraction for side legend") {
|
||||
MolDraw2DSVG drawer(200, 200, -1, -1, NO_FREETYPE);
|
||||
drawer.drawOptions().legendPosition = MolDrawOptions::LegendPosition::Left;
|
||||
drawer.drawOptions().legendFraction = 0.25f;
|
||||
drawer.drawOptions().legendVerticalText = true;
|
||||
MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1, "CCO");
|
||||
drawer.finishDrawing();
|
||||
auto text = drawer.getDrawingText();
|
||||
CHECK(text.find("class='legend'") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Github 5061 - draw reaction with no reagents and scaleBondWidth true") {
|
||||
SECTION("basics") {
|
||||
|
||||
@@ -151,10 +151,10 @@ static const std::map<std::string, std::hash_result_t> SVG_HASHES = {
|
||||
{"test21_2.svg", 3050176664U},
|
||||
{"test22_1.svg", 3688394300U},
|
||||
{"test22_2.svg", 1963311622U},
|
||||
{"testGithub3112_1.svg", 75452578U},
|
||||
{"testGithub3112_2.svg", 2379426157U},
|
||||
{"testGithub3112_3.svg", 102822156U},
|
||||
{"testGithub3112_4.svg", 497518508U},
|
||||
{"testGithub3112_1.svg", 1683049899U},
|
||||
{"testGithub3112_2.svg", 1452032589U},
|
||||
{"testGithub3112_3.svg", 3042134563U},
|
||||
{"testGithub3112_4.svg", 3952115653U},
|
||||
{"testGithub3305_1.svg", 3688394300U},
|
||||
{"testGithub3305_2.svg", 3172298658U},
|
||||
{"testGithub3305_3.svg", 30441258U},
|
||||
@@ -3858,11 +3858,11 @@ void testGithub3112() {
|
||||
#ifdef RDK_BUILD_FREETYPE_SUPPORT
|
||||
#if DO_TEST_ASSERT
|
||||
// this is the b (4th character)
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 134.6 185.3") !=
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 130.1 179.1") !=
|
||||
std::string::npos);
|
||||
#endif
|
||||
#else
|
||||
TEST_ASSERT(text.find("<text x='120.5' y='190.0' class='legend' "
|
||||
TEST_ASSERT(text.find("<text x='121.0' y='190.0' class='legend' "
|
||||
"style='font-size:16px;font-style:normal;font-weight:"
|
||||
"normal;fill-opacity:1;stroke:none;font-family:sans-"
|
||||
"serif;text-anchor:start;fill:#000000' >b</text>") !=
|
||||
@@ -3884,11 +3884,11 @@ void testGithub3112() {
|
||||
#ifdef RDK_BUILD_FREETYPE_SUPPORT
|
||||
#if DO_TEST_ASSERT
|
||||
// this is the b on the 2nd line.
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 119.7 184.3") !=
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 120.2 184.3") !=
|
||||
std::string::npos);
|
||||
#endif
|
||||
#else
|
||||
TEST_ASSERT(text.find("<text x='111.1' y='190.0' class='legend' "
|
||||
TEST_ASSERT(text.find("<text x='111.6' y='190.0' class='legend' "
|
||||
"style='font-size:11px;font-style:normal;font-weight:"
|
||||
"normal;fill-opacity:1;stroke:none;font-family:sans-"
|
||||
"serif;text-anchor:start;fill:#000000' >b</text>") !=
|
||||
@@ -3912,11 +3912,11 @@ void testGithub3112() {
|
||||
#ifdef RDK_BUILD_FREETYPE_SUPPORT
|
||||
#if DO_TEST_ASSERT
|
||||
// The first letter, N.
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 12.9 182.6") !=
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 13.4 182.6") !=
|
||||
std::string::npos);
|
||||
#endif
|
||||
#else
|
||||
TEST_ASSERT(text.find("<text x='9.7' y='190.0' class='legend' "
|
||||
TEST_ASSERT(text.find("<text x='10.2' y='190.0' class='legend' "
|
||||
"style='font-size:9px;font-style:normal;font-weight:"
|
||||
"normal;fill-opacity:1;stroke:none;font-family:sans-"
|
||||
"serif;text-anchor:start;fill:#000000' >N</text>") !=
|
||||
@@ -3940,11 +3940,11 @@ void testGithub3112() {
|
||||
#ifdef RDK_BUILD_FREETYPE_SUPPORT
|
||||
#if DO_TEST_ASSERT
|
||||
// The first letter, N
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 59.9 172.5") !=
|
||||
TEST_ASSERT(text.find("<path class='legend' d='M 60.4 172.5") !=
|
||||
std::string::npos);
|
||||
#endif
|
||||
#else
|
||||
TEST_ASSERT(text.find("<text x='57.2' y='181.0' class='legend' "
|
||||
TEST_ASSERT(text.find("<text x='57.7' y='181.0' class='legend' "
|
||||
"style='font-size:11px;font-style:normal;font-weight:"
|
||||
"normal;fill-opacity:1;stroke:none;font-family:sans-"
|
||||
"serif;text-anchor:start;fill:#000000' >N</text>") !=
|
||||
|
||||
@@ -640,7 +640,7 @@ There are additional operators defined in the cartridge, but these are used for
|
||||
- mol\_to\_pkl(mol) : returns binary string (bytea) for a molecule. (*available from Q3 2012 (2012\_09) release*)
|
||||
- mol\_to\_ctab(mol,bool default true, bool default false) : returns a CTAB (mol block) string for a molecule. The optional second argument controls whether or not 2D coordinates will be generated for molecules that don't have coordinates. The optional third argument (available since the 2021\_09 release) controls whether or not a V3000 ctab should be generated.
|
||||
- mol\_to\_v3kctab(mol,bool default true) : returns a CTAB (mol block) string for a molecule. The optional second argument controls whether or not 2D coordinates will be generated for molecules that don't have coordinates (*available from 2021\_09 release*).
|
||||
- mol\_to\_svg(mol,string default '',int default 250, int default 200, string default '') : returns an SVG with a drawing of the molecule. The optional parameters are a string to use as the legend, the width of the image, the height of the image, and a JSON with additional rendering parameters. (*available from the 2016\_09 release*)
|
||||
- mol\_to\_svg(mol,string default '',int default 250, int default 200, string default '') : returns an SVG with a drawing of the molecule. The optional parameters are a string to use as the legend, the width of the image, the height of the image, and a JSON with additional rendering parameters. The JSON may include ``legendPosition`` (``"Top"``, ``"Left"``, ``"Right"``, ``"Bottom"``) and ``legendVerticalText`` (boolean, for side legends). (*available from the 2016\_09 release*)
|
||||
- mol\_to\_json(string) : returns the commonchem JSON for a molecule. (*available from the 2021\_09 release*)
|
||||
- mol\_from\_json(string) : returns a molecule for a commonchem JSON string, NULL if the molecule construction fails. (*available from the 2021\_09 release*)
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ if _sip_available():
|
||||
|
||||
def MolToImage(mol, size=(300, 300), kekulize=True, wedgeBonds=True, fitImage=False, options=None,
|
||||
**kwargs):
|
||||
"""Returns a PIL image containing a drawing of the molecule
|
||||
"""Returns a PIL image containing a drawing of the molecule.
|
||||
|
||||
ARGUMENTS:
|
||||
|
||||
@@ -84,6 +84,14 @@ def MolToImage(mol, size=(300, 300), kekulize=True, wedgeBonds=True, fitImage=Fa
|
||||
|
||||
- highlightColor: RGB color as tuple (default [1, 0, 0])
|
||||
|
||||
- legend: optional text drawn as legend (default ''). Position and style
|
||||
are controlled by options (MolDrawOptions): legendPosition (Bottom, Top,
|
||||
Left, Right), legendVerticalText (for Left/Right), legendFontSize,
|
||||
legendFraction. Pass as options=opts or drawOptions=opts.
|
||||
|
||||
- options: rdMolDraw2D.MolDrawOptions instance for drawing options
|
||||
(e.g. legendPosition, legendVerticalText). Single-molecule only.
|
||||
|
||||
NOTE:
|
||||
|
||||
use 'matplotlib.colors.to_rgb()' to convert string and
|
||||
@@ -139,6 +147,24 @@ def MolToFile(mol, filename, size=(300, 300), kekulize=True, wedgeBonds=True, im
|
||||
outf.close()
|
||||
|
||||
|
||||
def MolToSVG(mol, size=(300, 300), kekulize=True, wedgeBonds=True, drawOptions=None,
|
||||
**kwargs):
|
||||
"""Returns an SVG string containing a drawing of the molecule.
|
||||
|
||||
Supports the same arguments as MolToImage for highlights and legend.
|
||||
For legend position (Top/Left/Right/Bottom) and vertical text on side legends,
|
||||
pass an rdMolDraw2D.MolDrawOptions instance with legendPosition and
|
||||
legendVerticalText set. Legend position applies to single-molecule drawing;
|
||||
for grid drawing (MolsToGridImage), pass the same drawOptions and the position
|
||||
applies to each cell's legend.
|
||||
"""
|
||||
if not mol:
|
||||
raise ValueError('Null molecule provided')
|
||||
return _moltoSVG(mol, size, kwargs.get('highlightAtoms', []), kwargs.get('legend', ''),
|
||||
kekulize, drawOptions=drawOptions,
|
||||
highlightBonds=kwargs.get('highlightBonds', []))
|
||||
|
||||
|
||||
def ShowMol(mol, size=(300, 300), kekulize=True, wedgeBonds=True, title='RDKit Molecule',
|
||||
stayInFront=True, **kwargs):
|
||||
""" Generates a picture of a molecule and displays it in a Tkinter window
|
||||
|
||||
Reference in New Issue
Block a user