Skip to content

Commit

Permalink
closes #238: Domain colors
Browse files Browse the repository at this point in the history
  • Loading branch information
dave-doty committed Dec 3, 2022
1 parent 5312a59 commit 1b7e605
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 15 deletions.
83 changes: 73 additions & 10 deletions scadnano/scadnano.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,25 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> s
# return NoIndent(self.__dict__) if suppress_indent else self.__dict__
return f'#{self.r:02x}{self.g:02x}{self.b:02x}'

@staticmethod
def from_json(color_json: Union[str, int, None]) -> Union['Color', None]:
if color_json is None:
return None

color_str: str
if isinstance(color_json, int):
def decimal_int_to_hex(d: int) -> str:
return "#" + "{0:#08x}".format(d, 8)[2:] # type: ignore

color_str = decimal_int_to_hex(color_json)
elif isinstance(color_json, str):
color_str = color_json
else:
raise IllegalDesignError(f'color must be a string or int, '
f'but it is a {type(color_json)}: {color_json}')
color = Color(hex_string=color_str)
return color

def to_cadnano_v2_int_hex(self) -> int:
return int(f'{self.r:02x}{self.g:02x}{self.b:02x}', 16)

Expand Down Expand Up @@ -1679,6 +1698,11 @@ class Domain(_JSONSerializable, Generic[DomainLabel]):
"""DNA sequence of this Domain, or ``None`` if no DNA sequence has been assigned
to this :any:`Domain`'s :any:`Strand`."""

color: Optional[Color] = None
"""
Color to show this domain in the main view. If specified, overrides the field :data:`Strand.color`.
"""

# not serialized; for efficiency
# remove quotes when Py3.6 support dropped
_parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None)
Expand All @@ -1701,6 +1725,8 @@ def to_json_serializable(self, suppress_indent: bool = True,
dct[insertions_key] = self.insertions
if self.label is not None:
dct[domain_label_key] = self.label
if self.color is not None:
dct[color_key] = self.color.to_json_serializable(suppress_indent)
return NoIndent(dct) if suppress_indent else dct

@staticmethod
Expand All @@ -1714,6 +1740,8 @@ def from_json(json_map: Dict[str, Any]) -> 'Domain': # remove quotes when Py3.6
list(map(tuple, json_map.get(insertions_key, [])))) # type: ignore
name = json_map.get(domain_name_key)
label = json_map.get(domain_label_key)
color_json = json_map.get(color_key)
color = Color.from_json(color_json)
return Domain(
helix=helix,
forward=forward,
Expand All @@ -1723,6 +1751,7 @@ def from_json(json_map: Dict[str, Any]) -> 'Domain': # remove quotes when Py3.6
insertions=insertions,
name=name,
label=label,
color=color,
)

def __repr__(self) -> str:
Expand All @@ -1734,6 +1763,7 @@ def __repr__(self) -> str:
f', end={self.end}') + \
(f', deletions={self.deletions}' if len(self.deletions) > 0 else '') + \
(f', insertions={self.insertions}' if len(self.insertions) > 0 else '') + \
(f', color={self.color}' if self.color is not None else '') + \
')'
return rep

Expand Down Expand Up @@ -2077,6 +2107,11 @@ class Loopout(_JSONSerializable, Generic[DomainLabel]):
dna_sequence: Optional[str] = None
"""DNA sequence of this :any:`Loopout`, or ``None`` if no DNA sequence has been assigned."""

color: Optional[Color] = None
"""
Color to show this loopout in the main view. If specified, overrides the field :data:`Strand.color`.
"""

# not serialized; for efficiency
# remove quotes when Py3.6 support dropped
_parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None)
Expand All @@ -2088,6 +2123,8 @@ def to_json_serializable(self, suppress_indent: bool = True,
dct[domain_name_key] = self.name
if self.label is not None:
dct[domain_label_key] = self.label
if self.color is not None:
dct[color_key] = self.color.to_json_serializable(suppress_indent)
return NoIndent(dct) if suppress_indent else dct

@staticmethod
Expand All @@ -2098,7 +2135,9 @@ def from_json(json_map: Dict[str, Any]) -> 'Loopout': # remove quotes when Py3.
length = int(length_str)
name = json_map.get(domain_name_key)
label = json_map.get(domain_label_key)
return Loopout(length=length, name=name, label=label)
color_json = json_map.get(color_key)
color = Color.from_json(color_json)
return Loopout(length=length, name=name, label=label, color=color)

def strand(self) -> 'Strand': # remove quotes when Py3.6 support dropped
"""
Expand Down Expand Up @@ -2232,6 +2271,11 @@ class Extension(_JSONSerializable, Generic[DomainLabel]):
dna_sequence: Optional[str] = None
"""DNA sequence of this :any:`Extension`, or ``None`` if no DNA sequence has been assigned."""

color: Optional[Color] = None
"""
Color to show this extension in the main view. If specified, overrides the field :data:`Strand.color`.
"""

# not serialized; for efficiency
# remove quotes when Py3.6 support dropped
_parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None)
Expand All @@ -2243,6 +2287,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) \
self._add_display_angle_if_not_default(json_map)
self._add_name_if_not_default(json_map)
self._add_label_if_not_default(json_map)
if self.color is not None:
json_map[color_key] = self.color.to_json_serializable(suppress_indent)
return NoIndent(json_map) if suppress_indent else json_map

def dna_length(self) -> int:
Expand All @@ -2269,11 +2315,16 @@ def float_transformer(x): return float(x)
json_map, display_angle_key, transformer=float_transformer)
name = json_map.get(domain_name_key)
label = json_map.get(domain_label_key)
color_json = json_map.get(color_key)
color = Color.from_json(color_json)
return Extension(
num_bases=num_bases,
display_length=display_length,
display_angle=display_angle,
label=label, name=name)
label=label,
name=name,
color=color,
)

def _add_display_length_if_not_default(self, json_map) -> None:
self._add_key_value_to_json_map_if_not_default(
Expand Down Expand Up @@ -2839,6 +2890,23 @@ def with_domain_sequence(self, sequence: str, assign_complement: bool = False) \
assign_complement=assign_complement)
return self

# remove quotes when Py3.6 support dropped
def with_domain_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]':
"""
Sets most recent :any:`Domain`/:any:`Loopout`/:any:`Extension`
to have given color.
:param color:
color to set for :any:`Domain`/:any:`Loopout`/:any:`Extension`
:return:
self
"""
if self._strand is None:
raise ValueError('no Strand created yet; make at least one domain first')
last_domain = self._strand.domains[-1]
last_domain.color = color
return self

# remove quotes when Py3.6 support dropped
def with_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]':
"""
Expand Down Expand Up @@ -3253,17 +3321,12 @@ def from_json(json_map: dict) -> 'Strand': # remove quotes when Py3.6 support d

dna_sequence = optional_field(None, json_map, dna_sequence_key, legacy_keys=legacy_dna_sequence_keys)

color_str = json_map.get(color_key)
color_json = json_map.get(color_key)

if color_str is None:
if color_json is None:
color = default_scaffold_color if is_scaffold else default_strand_color
else:
if isinstance(color_str, int):
def decimal_int_to_hex(d: int) -> str:
return "#" + "{0:#08x}".format(d, 8)[2:] # type: ignore

color_str = decimal_int_to_hex(color_str)
color = Color(hex_string=color_str)
color = Color.from_json(color_json)

label = json_map.get(strand_label_key)

Expand Down
15 changes: 10 additions & 5 deletions tests/scadnano_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,43 +67,48 @@ def test_strand__loopouts_with_labels(self) -> None:
self.assertEqual(1, len(design.strands))
self.assertEqual(expected_strand, design.strands[0])

def test_strand__loopouts_with_labels_to_json(self) -> None:
def test_strand__loopouts_with_labels_and_colors_to_json(self) -> None:
design = self.design_6helix
sb = design.draw_strand(0, 0)
sb.to(10)
sb.loopout(1, 8)
sb.with_domain_color(sc.Color(10, 10, 10))
sb.with_domain_label('loop0')
sb.to(5)
sb.with_domain_label('dom1')
sb.with_domain_color(sc.Color(20, 20, 20))
sb.cross(2)
sb.to(10)
sb.with_domain_label('dom2')
sb.loopout(3, 12)
sb.with_domain_label('loop1')
sb.to(5)
sb.with_color(sc.Color(30, 30, 30))
design_json_map = design.to_json_serializable(suppress_indent=False)
design_from_json = sc.Design.from_scadnano_json_map(design_json_map)
expected_strand = sc.Strand([
sc.Domain(0, True, 0, 10),
sc.Loopout(8, label='loop0'),
sc.Domain(1, False, 5, 10, label='dom1'),
sc.Loopout(8, label='loop0', color=sc.Color(10, 10, 10)),
sc.Domain(1, False, 5, 10, label='dom1', color=sc.Color(20, 20, 20)),
sc.Domain(2, True, 5, 10, label='dom2'),
sc.Loopout(12, label='loop1'),
sc.Domain(3, False, 5, 10),
])
], color=sc.Color(30, 30, 30))
self.assertEqual(1, len(design_from_json.strands))
self.assertEqual(expected_strand, design_from_json.strands[0])
self.assertEqual(expected_strand.color, sc.Color(30, 30, 30))

def test_strand__3p_extension(self) -> None:
design = self.design_6helix
sb = design.draw_strand(0, 0)
sb.to(10)

sb.extension_3p(5)
sb.with_domain_color(sc.Color(10, 10, 10))

expected_strand: sc.Strand = sc.Strand([
sc.Domain(0, True, 0, 10),
sc.Extension(num_bases=5),
sc.Extension(num_bases=5, color=sc.Color(10, 10, 10)),
])
self.assertEqual(1, len(design.strands))
self.assertEqual(expected_strand, design.strands[0])
Expand Down

0 comments on commit 1b7e605

Please sign in to comment.