Skip to content

Commit f58a960

Browse files
committed
Add support for custom SVG cell colors and validation in PhysiCell settings
- Introduced a new XML structure for specifying cell colors in PhysiCell_settings.xml. - Implemented validation for SVG/CSS color formats in PhysiCell_pathology.cpp. - Updated PhysiCell_Settings to read custom colors from XML and store them in a map. - Enhanced color assignment logic in paint_by_number_cell_coloring to accommodate user-defined colors.
1 parent 2354b24 commit f58a960

5 files changed

Lines changed: 216 additions & 32 deletions

File tree

config/PhysiCell_settings.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
<SVG>
3636
<interval units="min">60</interval>
3737
<enable>true</enable>
38+
<!-- Optional: set SVG colors per cell type.
39+
If this element is present, every cell definition must have exactly one entry
40+
(names must match cell_definition names exactly); any mismatch or omission is an error.
41+
If absent, built-in colors are used and extra types beyond the palette are auto-generated.
42+
<cell_colors>
43+
<cell_color name="default cell">grey</cell_color>
44+
</cell_colors>
45+
-->
3846
</SVG>
3947
<legacy_data>
4048
<enable>false</enable>

modules/PhysiCell_pathology.cpp

Lines changed: 184 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,40 +1707,197 @@ std::string paint_by_density_percentage(double concentration, double max_conc, d
17071707
return colormap[ind];
17081708
}
17091709

1710+
// Returns true if the string is a valid SVG/CSS color:
1711+
// - CSS named color (case-insensitive)
1712+
// - #rgb or #rrggbb hex notation
1713+
// - rgb(r,g,b) functional notation (integers 0-255)
1714+
static bool is_valid_svg_color( const std::string& s )
1715+
{
1716+
if( s.empty() ) return false;
1717+
1718+
// #rgb / #rrggbb
1719+
if( s[0] == '#' )
1720+
{
1721+
if( s.size() != 4 && s.size() != 7 ) return false;
1722+
for( size_t i = 1; i < s.size(); i++ )
1723+
{
1724+
char c = s[i];
1725+
if( !( (c>='0'&&c<='9') || (c>='a'&&c<='f') || (c>='A'&&c<='F') ) )
1726+
return false;
1727+
}
1728+
return true;
1729+
}
1730+
1731+
// rgb(r,g,b) — accept optional whitespace, integers 0-255
1732+
if( s.size() > 4 && s.substr(0,4) == "rgb(" && s.back() == ')' )
1733+
{
1734+
std::string inner = s.substr(4, s.size()-5);
1735+
// parse three comma-separated integers
1736+
int vals[3]; int nread;
1737+
char extra;
1738+
nread = std::sscanf(inner.c_str(), " %d , %d , %d %c", &vals[0], &vals[1], &vals[2], &extra);
1739+
if( nread != 3 ) return false;
1740+
for( int i = 0; i < 3; i++ )
1741+
if( vals[i] < 0 || vals[i] > 255 ) return false;
1742+
return true;
1743+
}
1744+
1745+
// CSS named colors (standard 147 + rebeccapurple)
1746+
static const std::unordered_set<std::string> named = {
1747+
"aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black",
1748+
"blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse",
1749+
"chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan",
1750+
"darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta",
1751+
"darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen",
1752+
"darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink",
1753+
"deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen",
1754+
"fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","green","greenyellow",
1755+
"grey","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender",
1756+
"lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan",
1757+
"lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon",
1758+
"lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue",
1759+
"lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine",
1760+
"mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue",
1761+
"mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream",
1762+
"mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange",
1763+
"orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred",
1764+
"papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple",
1765+
"red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen",
1766+
"seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow",
1767+
"springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet",
1768+
"wheat","white","whitesmoke","yellow","yellowgreen"
1769+
};
1770+
// compare lower-case
1771+
std::string lower = s;
1772+
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
1773+
return named.count(lower) > 0;
1774+
}
1775+
1776+
// Convert HSV (h in [0,360), s and v in [0,1]) to an SVG "rgb(r,g,b)" string.
1777+
static std::string hsv_to_svg_color( double h, double s, double v )
1778+
{
1779+
double c = v * s;
1780+
double x = c * ( 1.0 - std::fabs( std::fmod( h / 60.0, 2.0 ) - 1.0 ) );
1781+
double m = v - c;
1782+
double r1, g1, b1;
1783+
if ( h < 60 ) { r1 = c; g1 = x; b1 = 0; }
1784+
else if( h < 120 ) { r1 = x; g1 = c; b1 = 0; }
1785+
else if( h < 180 ) { r1 = 0; g1 = c; b1 = x; }
1786+
else if( h < 240 ) { r1 = 0; g1 = x; b1 = c; }
1787+
else if( h < 300 ) { r1 = x; g1 = 0; b1 = c; }
1788+
else { r1 = c; g1 = 0; b1 = x; }
1789+
int r = (int)std::round( (r1 + m) * 255 );
1790+
int g = (int)std::round( (g1 + m) * 255 );
1791+
int b = (int)std::round( (b1 + m) * 255 );
1792+
return "rgb(" + std::to_string(r) + "," + std::to_string(g) + "," + std::to_string(b) + ")";
1793+
}
1794+
17101795
std::vector<std::string> paint_by_number_cell_coloring( Cell* pCell )
17111796
{
1712-
static std::vector< std::string > colors(0);
1713-
static bool setup_done = false;
1797+
static std::vector<std::string> colors;
1798+
static bool setup_done = false;
17141799
if( setup_done == false )
17151800
{
1716-
colors.push_back( "grey" ); // default color will be grey
1801+
// Built-in palette for the first 13 cell types.
1802+
static const std::vector<std::string> builtin = {
1803+
"grey", "red", "yellow", "green", "blue",
1804+
"magenta", "orange", "lime", "cyan",
1805+
"hotpink", "peachpuff", "darkseagreen", "lightskyblue"
1806+
};
17171807

1718-
colors.push_back( "red" );
1719-
colors.push_back( "yellow" );
1720-
colors.push_back( "green" );
1721-
colors.push_back( "blue" );
1722-
1723-
colors.push_back( "magenta" );
1724-
colors.push_back( "orange" );
1725-
colors.push_back( "lime" );
1726-
colors.push_back( "cyan" );
1727-
1728-
colors.push_back( "hotpink" );
1729-
colors.push_back( "peachpuff" );
1730-
colors.push_back( "darkseagreen" );
1731-
colors.push_back( "lightskyblue" );
1808+
int n = (int)cell_definitions_by_index.size();
1809+
1810+
if( PhysiCell_settings.svg_cell_colors_specified )
1811+
{
1812+
// Validate: every user-supplied name must match a cell definition.
1813+
for( auto& kv : PhysiCell_settings.svg_cell_colors_by_name )
1814+
{
1815+
if( cell_definitions_by_name.find(kv.first) == cell_definitions_by_name.end() )
1816+
{
1817+
std::cerr << "ERROR (paint_by_number_cell_coloring): <cell_color name=\""
1818+
<< kv.first << "\"> does not match any cell definition." << std::endl;
1819+
exit(-1);
1820+
}
1821+
}
1822+
// Validate: every supplied color value must be a recognized SVG color.
1823+
for( auto& kv : PhysiCell_settings.svg_cell_colors_by_name )
1824+
{
1825+
if( !is_valid_svg_color(kv.second) )
1826+
{
1827+
std::cerr << "ERROR (paint_by_number_cell_coloring): <cell_color name=\""
1828+
<< kv.first << "\"> has unrecognized color value \""
1829+
<< kv.second << "\"." << std::endl;
1830+
exit(-1);
1831+
}
1832+
}
1833+
// Build the pool of default colors not already claimed by the user (case-insensitive).
1834+
std::unordered_set<std::string> claimed;
1835+
for( auto& kv : PhysiCell_settings.svg_cell_colors_by_name )
1836+
{
1837+
std::string lc = kv.second;
1838+
std::transform(lc.begin(), lc.end(), lc.begin(), ::tolower);
1839+
claimed.insert(lc);
1840+
}
1841+
std::vector<std::string> pool;
1842+
for( auto& c : builtin )
1843+
{
1844+
if( claimed.count(c) == 0 )
1845+
{ pool.push_back(c); }
1846+
}
17321847

1733-
setup_done = true;
1848+
// Assign colors: user-specified first, then pool, then HSV-generated.
1849+
colors.resize(n);
1850+
int pool_idx = 0;
1851+
int extra_idx = 0;
1852+
for( int i = 0; i < n; i++ )
1853+
{
1854+
const std::string& name = cell_definitions_by_index[i]->name;
1855+
auto it = PhysiCell_settings.svg_cell_colors_by_name.find(name);
1856+
if( it != PhysiCell_settings.svg_cell_colors_by_name.end() )
1857+
{
1858+
colors[i] = it->second;
1859+
}
1860+
else if( pool_idx < (int)pool.size() )
1861+
{
1862+
colors[i] = pool[pool_idx++];
1863+
}
1864+
else
1865+
{
1866+
double hue = std::fmod( extra_idx * 137.508, 360.0 );
1867+
colors[i] = hsv_to_svg_color( hue, 0.65, 0.85 );
1868+
extra_idx++;
1869+
}
1870+
}
1871+
}
1872+
else
1873+
{
1874+
// No user colors: use built-in palette; generate via golden-angle HSV for extras.
1875+
colors.resize(n);
1876+
int n_extra = 0;
1877+
for( int i = 0; i < n; i++ )
1878+
{
1879+
if( i < (int)builtin.size() )
1880+
{ colors[i] = builtin[i]; }
1881+
else
1882+
{
1883+
double hue = std::fmod( n_extra * 137.508, 360.0 );
1884+
colors[i] = hsv_to_svg_color( hue, 0.65, 0.85 );
1885+
n_extra++;
1886+
}
1887+
}
1888+
}
1889+
1890+
setup_done = true;
17341891
}
1735-
1736-
// start all black
1737-
1738-
std::vector<std::string> output = { "black", "black", "black", "black" };
1739-
1740-
// paint by number -- by cell type
1741-
1742-
std::string interior_color = "white";
1743-
if( pCell->type < 13 )
1892+
1893+
// start all black
1894+
1895+
std::vector<std::string> output = { "black", "black", "black", "black" };
1896+
1897+
// paint by number -- by cell type
1898+
1899+
std::string interior_color = "white";
1900+
if( pCell->type >= 0 && pCell->type < (int)colors.size() )
17441901
{ interior_color = colors[ pCell->type ]; }
17451902

17461903
output[0] = interior_color; // set cytoplasm color

modules/PhysiCell_pathology.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@
6565
###############################################################################
6666
*/
6767

68-
#include <vector>
68+
#include <algorithm>
6969
#include <string>
70+
#include <unordered_set>
71+
#include <vector>
7072

7173
#ifndef __PhysiCell_pathology__
7274
#define __PhysiCell_pathology__

modules/PhysiCell_settings.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,22 @@ void PhysiCell_Settings::read_from_pugixml( void )
211211
enable_full_saves = xml_get_bool_value( node , "enable" );
212212
node = node.parent();
213213

214-
node = xml_find_node( node , "SVG" );
214+
node = xml_find_node( node , "SVG" );
215215
SVG_save_interval = xml_get_double_value( node , "interval" );
216-
enable_SVG_saves = xml_get_bool_value( node , "enable" );
216+
enable_SVG_saves = xml_get_bool_value( node , "enable" );
217+
218+
pugi::xml_node node_cell_colors = node.child("cell_colors");
219+
if( node_cell_colors )
220+
{
221+
svg_cell_colors_specified = true;
222+
for( pugi::xml_node cc = node_cell_colors.child("cell_color"); cc; cc = cc.next_sibling("cell_color") )
223+
{
224+
std::string cname = cc.attribute("name").as_string();
225+
std::string color = cc.child_value();
226+
if( !cname.empty() && !color.empty() )
227+
{ svg_cell_colors_by_name[cname] = color; }
228+
}
229+
}
217230

218231
pugi::xml_node node_plot_substrate;
219232
node_plot_substrate = xml_find_node( node , "plot_substrate" );

modules/PhysiCell_settings.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
#include <vector>
7676
#include <random>
7777
#include <chrono>
78+
#include <map>
7879
#include <unordered_map>
7980

8081
#include "./PhysiCell_pugixml.h"
@@ -115,8 +116,11 @@ class PhysiCell_Settings
115116

116117
bool disable_automated_spring_adhesions = false;
117118

118-
double SVG_save_interval = 60;
119-
bool enable_SVG_saves = true;
119+
double SVG_save_interval = 60;
120+
bool enable_SVG_saves = true;
121+
122+
bool svg_cell_colors_specified = false;
123+
std::map<std::string, std::string> svg_cell_colors_by_name;
120124

121125
bool enable_substrate_plot = false;
122126
std::string substrate_to_monitor = "oxygen";

0 commit comments

Comments
 (0)