marsilea 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
marsilea/dataset.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import pandas as pd
2
+ import numpy as np
2
3
  from platformdirs import user_cache_path
3
4
  from urllib.request import urlretrieve
4
5
 
@@ -16,7 +17,8 @@ def load_data(name, cache=True):
16
17
  (TCGA, PanCancer Atlas)
17
18
  - 'mouse_embryo': Spatial mapping of mouse embryo at E12.5
18
19
  - 'seq_align': Sequence alignment data
19
- - 'les_miserables': Characters in Les Miserables
20
+ - 'les_miserables': Characters in Les Misérables
21
+ - 'sc_multiomics': Single-cell multi-omics dataset from COVID-19 patients
20
22
 
21
23
  Parameters
22
24
  ----------
@@ -33,6 +35,8 @@ def load_data(name, cache=True):
33
35
  return _load_imdb(cache)
34
36
  elif name == "pbmc3k":
35
37
  return _load_pbmc3k(cache)
38
+ elif name == "sc_multiomics":
39
+ return _load_sc_multiomics(cache)
36
40
  elif name == "oncoprint":
37
41
  return _load_oncoprint(cache)
38
42
  elif name == "mouse_embryo":
@@ -61,7 +65,11 @@ def _cache_remote(files, cache=True):
61
65
  dest = data_dir / dfname
62
66
  # Not download when cache and exist
63
67
  if not (cache and dest.exists()):
64
- urlretrieve(url, dest)
68
+ try:
69
+ urlretrieve(url, dest)
70
+ except Exception as e:
71
+ print(url)
72
+ raise e
65
73
 
66
74
  if len(files) == 1:
67
75
  return data_dir / download_files[0]
@@ -75,28 +83,50 @@ def _load_imdb(cache=True):
75
83
 
76
84
 
77
85
  def _load_pbmc3k(cache=True):
78
- exp, pct_cells, count = _cache_remote(['pbmc3k/exp.csv',
79
- 'pbmc3k/pct_cells.csv',
80
- 'pbmc3k/count.csv'],
81
- cache=cache)
86
+ exp, pct_cells, count = _cache_remote(
87
+ ["pbmc3k/exp.csv", "pbmc3k/pct_cells.csv", "pbmc3k/count.csv"], cache=cache
88
+ )
82
89
  return {
83
- 'exp': pd.read_csv(exp, index_col=0),
84
- 'pct_cells': pd.read_csv(pct_cells, index_col=0),
85
- 'count': pd.read_csv(count, index_col=0)
90
+ "exp": pd.read_csv(exp, index_col=0),
91
+ "pct_cells": pd.read_csv(pct_cells, index_col=0),
92
+ "count": pd.read_csv(count, index_col=0),
86
93
  }
87
94
 
88
95
 
96
+ def _load_sc_multiomics(cache=True):
97
+ stack, interaction = _cache_remote(
98
+ [
99
+ "sc-multiomics/sc-multiomics.npz",
100
+ "sc-multiomics/sc-multiomics-interaction.csv",
101
+ ]
102
+ )
103
+
104
+ dataset = np.load(stack, allow_pickle=True)
105
+ interaction = pd.read_csv(interaction)
106
+
107
+ data = {}
108
+ for key in dataset.files:
109
+ data[key] = dataset[key]
110
+ data["interaction"] = interaction
111
+
112
+ return data
113
+
114
+
89
115
  def _load_oncoprint(cache=True):
90
116
  cna, mrna, methyl, clinical = _cache_remote(
91
- ['oncoprint/cna.csv', 'oncoprint/mrna_exp.csv',
92
- 'oncoprint/methyl_exp.csv', 'oncoprint/clinical.csv'],
93
- cache=cache
117
+ [
118
+ "oncoprint/cna.csv",
119
+ "oncoprint/mrna_exp.csv",
120
+ "oncoprint/methyl_exp.csv",
121
+ "oncoprint/clinical.csv",
122
+ ],
123
+ cache=cache,
94
124
  )
95
125
  return {
96
- 'cna': pd.read_csv(cna, index_col=0),
97
- 'mrna_exp': pd.read_csv(mrna, index_col=0),
98
- 'methyl_exp': pd.read_csv(methyl, index_col=0),
99
- 'clinical': pd.read_csv(clinical, index_col=0)
126
+ "cna": pd.read_csv(cna, index_col=0),
127
+ "mrna_exp": pd.read_csv(mrna, index_col=0),
128
+ "methyl_exp": pd.read_csv(methyl, index_col=0),
129
+ "clinical": pd.read_csv(clinical, index_col=0),
100
130
  }
101
131
 
102
132
 
@@ -117,11 +147,7 @@ def _load_cooking_oils(cache=True):
117
147
 
118
148
  def _load_les_miserables(cache=True):
119
149
  nodes, links = _cache_remote(
120
- ['les-miserables/miserables_nodes.csv',
121
- 'les-miserables/miserables_links.csv'
122
- ], cache=cache
150
+ ["les-miserables/miserables_nodes.csv", "les-miserables/miserables_links.csv"],
151
+ cache=cache,
123
152
  )
124
- return {
125
- 'nodes': pd.read_csv(nodes),
126
- 'links': pd.read_csv(links)
127
- }
153
+ return {"nodes": pd.read_csv(nodes), "links": pd.read_csv(links)}
marsilea/dendrogram.py CHANGED
@@ -8,17 +8,17 @@ from typing import List, Sequence
8
8
 
9
9
 
10
10
  class _DendrogramBase:
11
-
12
11
  is_singleton = False
13
12
 
14
- def __init__(self,
15
- data,
16
- method=None,
17
- metric=None,
18
- linkage=None,
19
- get_meta_center=None,
20
- key=None,
21
- ):
13
+ def __init__(
14
+ self,
15
+ data,
16
+ method=None,
17
+ metric=None,
18
+ linkage=None,
19
+ get_meta_center=None,
20
+ key=None,
21
+ ):
22
22
  self.key = key
23
23
  self.data = data
24
24
  if method is None:
@@ -29,8 +29,8 @@ class _DendrogramBase:
29
29
  if len(data) == 1:
30
30
  # TODO: the y coords are heuristic value,
31
31
  # need a better way to handle
32
- self.x_coords = np.array([[1., 1., 1., 1.]])
33
- self.y_coords = np.array([[0., .75, .75, 0.]])
32
+ self.x_coords = np.array([[1.0, 1.0, 1.0, 1.0]])
33
+ self.y_coords = np.array([[0.0, 0.75, 0.75, 0.0]])
34
34
  self._reorder_index = np.array([0])
35
35
  self.is_singleton = True
36
36
  else:
@@ -40,21 +40,21 @@ class _DendrogramBase:
40
40
  self.Z = scipy_linkage(data, method=method, metric=metric)
41
41
  self._plot_data = dendrogram(self.Z, no_plot=True)
42
42
 
43
- self.x_coords = np.asarray(self._plot_data['icoord']) / 5
44
- self.y_coords = np.asarray(self._plot_data['dcoord'])
45
- self._reorder_index = self._plot_data['leaves']
43
+ self.x_coords = np.asarray(self._plot_data["icoord"]) / 5
44
+ self.y_coords = np.asarray(self._plot_data["dcoord"])
45
+ self._reorder_index = self._plot_data["leaves"]
46
46
 
47
47
  if len(self.y_coords) == 1:
48
- self.y_coords = np.array([[0., .75, .75, 0.]])
48
+ self.y_coords = np.array([[0.0, 0.75, 0.75, 0.0]])
49
49
  else:
50
50
  ycoords = np.unique(self.y_coords)
51
51
  ycoords = ycoords[np.nonzero(ycoords)]
52
52
  y_min, y_max = np.min(ycoords), np.max(ycoords)
53
53
  interval = y_max - y_min
54
54
  for i, j in zip(*np.nonzero(self.y_coords)):
55
- if self.y_coords[i, j] != 0.:
55
+ if self.y_coords[i, j] != 0.0:
56
56
  v = self.y_coords[i, j]
57
- self.y_coords[i, j] = (v - y_min) / interval + .2
57
+ self.y_coords[i, j] = (v - y_min) / interval + 0.2
58
58
  # self.y_coords[i, j] -= offset
59
59
  # self.y_coords[np.nonzero(self.y_coords)] - offset
60
60
 
@@ -80,7 +80,8 @@ class _DendrogramBase:
80
80
  else:
81
81
  raise ValueError(
82
82
  "The get_meta_center must return a numpy array with shape "
83
- "matching the number of features in the data.")
83
+ "matching the number of features in the data."
84
+ )
84
85
  else:
85
86
  raise TypeError("The get_meta_center must be a callable function or None.")
86
87
 
@@ -107,8 +108,9 @@ class _DendrogramBase:
107
108
 
108
109
  if y_end is not None:
109
110
  if y_end < self.ylim[1]:
110
- raise ValueError(f"{y_end} is lower than "
111
- f"current ylim at {self.ylim[1]}")
111
+ raise ValueError(
112
+ f"{y_end} is lower than " f"current ylim at {self.ylim[1]}"
113
+ )
112
114
  self._render_ylim = (0, y_end)
113
115
 
114
116
  @property
@@ -123,7 +125,7 @@ class _DendrogramBase:
123
125
  def root(self):
124
126
  xc = self.x_coords[-1]
125
127
  yc = self.y_coords[-1]
126
- x1 = (xc[2] - xc[1]) / 2. + xc[1]
128
+ x1 = (xc[2] - xc[1]) / 2.0 + xc[1]
127
129
  y1 = yc[1]
128
130
  return x1, y1
129
131
 
@@ -131,11 +133,11 @@ class _DendrogramBase:
131
133
  def render_root(self):
132
134
  xc = self._render_x_coords[-1]
133
135
  yc = self._render_y_coords[-1]
134
- x1 = (xc[2] - xc[1]) / 2. + xc[1]
136
+ x1 = (xc[2] - xc[1]) / 2.0 + xc[1]
135
137
  y1 = yc[1]
136
138
  return x1, y1
137
139
 
138
- def _draw_dendrogram(self, ax, orient="top", color=".1", linewidth=.7):
140
+ def _draw_dendrogram(self, ax, orient="top", color=".1", linewidth=0.7):
139
141
  x_coords = self._render_x_coords
140
142
  y_coords = self._render_y_coords
141
143
  if orient in ["right", "left"]:
@@ -143,14 +145,15 @@ class _DendrogramBase:
143
145
 
144
146
  lines = LineCollection(
145
147
  [list(zip(x, y)) for x, y in zip(x_coords, y_coords)],
146
- color=color, linewidth=linewidth
148
+ color=color,
149
+ linewidth=linewidth,
147
150
  )
148
151
  ax.add_collection(lines)
149
152
 
150
153
 
151
154
  class Dendrogram(_DendrogramBase):
152
155
  """A dendrogram class
153
-
156
+
154
157
  Parameters
155
158
  ----------
156
159
 
@@ -159,26 +162,39 @@ class Dendrogram(_DendrogramBase):
159
162
  Refer to :func:`scipy.cluster.hierarchy.linkage`
160
163
  metric : str
161
164
  Refer to :func:`scipy.cluster.hierarchy.linkage`
162
-
165
+
163
166
  """
164
167
 
165
- def __init__(self,
166
- data: np.ndarray,
167
- method=None,
168
- metric=None,
169
- linkage=None,
170
- get_meta_center=None,
171
- key=None,
172
- ):
173
- super().__init__(data, method=method, metric=metric, key=key,
174
- linkage=linkage, get_meta_center=get_meta_center)
168
+ def __init__(
169
+ self,
170
+ data: np.ndarray,
171
+ method=None,
172
+ metric=None,
173
+ linkage=None,
174
+ get_meta_center=None,
175
+ key=None,
176
+ ):
177
+ super().__init__(
178
+ data,
179
+ method=method,
180
+ metric=metric,
181
+ key=key,
182
+ linkage=linkage,
183
+ get_meta_center=get_meta_center,
184
+ )
175
185
 
176
186
  # here we left an empty **kwargs to align api with GroupDendrogram
177
- def draw(self, ax, orient="top",
178
- color=None, linewidth=None,
179
- add_root=False, root_color=None,
180
- control_ax=True,
181
- **kwargs):
187
+ def draw(
188
+ self,
189
+ ax,
190
+ orient="top",
191
+ color=None,
192
+ linewidth=None,
193
+ add_root=False,
194
+ root_color=None,
195
+ control_ax=True,
196
+ **kwargs,
197
+ ):
182
198
  """
183
199
 
184
200
  Parameters
@@ -201,10 +217,9 @@ class Dendrogram(_DendrogramBase):
201
217
  """
202
218
  color = ".1" if color is None else color
203
219
  root_color = color if root_color is None else root_color
204
- linewidth = .7 if linewidth is None else linewidth
220
+ linewidth = 0.7 if linewidth is None else linewidth
205
221
 
206
- self._draw_dendrogram(ax, orient=orient, color=color,
207
- linewidth=linewidth)
222
+ self._draw_dendrogram(ax, orient=orient, color=color, linewidth=linewidth)
208
223
 
209
224
  xlim = self._render_xlim
210
225
  ylim = self._render_ylim
@@ -228,8 +243,9 @@ class Dendrogram(_DendrogramBase):
228
243
  else:
229
244
  x2 = x1
230
245
  y2 = ylim[1]
231
- root_line = Line2D([x1, x2], [y1, y2],
232
- color=root_color, linewidth=linewidth)
246
+ root_line = Line2D(
247
+ [x1, x2], [y1, y2], color=root_color, linewidth=linewidth
248
+ )
233
249
  ax.add_artist(root_line)
234
250
 
235
251
 
@@ -243,20 +259,22 @@ class GroupDendrogram(_DendrogramBase):
243
259
  A list of :class:`Dendrogram`
244
260
  method : str
245
261
  metric : str
246
-
262
+
247
263
  """
248
264
 
249
- def __init__(self,
250
- dens: List[Dendrogram],
251
- method=None,
252
- metric=None,
253
- get_meta_center=None,
254
- key=None,
255
- **kwargs,
256
- ):
265
+ def __init__(
266
+ self,
267
+ dens: List[Dendrogram],
268
+ method=None,
269
+ metric=None,
270
+ get_meta_center=None,
271
+ key=None,
272
+ **kwargs,
273
+ ):
257
274
  data = np.vstack([d.center for d in dens])
258
- super().__init__(data, method=method, metric=metric,
259
- get_meta_center=get_meta_center, key=key)
275
+ super().__init__(
276
+ data, method=method, metric=metric, get_meta_center=get_meta_center, key=key
277
+ )
260
278
  self.orig_dens = np.asarray(dens)
261
279
  self.dens = np.asarray(dens)[self.reorder_index]
262
280
  self.n = len(self.dens)
@@ -274,19 +292,20 @@ class GroupDendrogram(_DendrogramBase):
274
292
  self.den_ylim = ylim
275
293
  self.divider = ylim * 1.05
276
294
 
277
- def draw(self,
278
- ax,
279
- orient="top",
280
- spacing=None,
281
- add_meta=True,
282
- add_base=True,
283
- base_colors=None,
284
- meta_color=None,
285
- linewidth=None,
286
- divide=True,
287
- divide_style="--",
288
- meta_ratio=.2,
289
- ):
295
+ def draw(
296
+ self,
297
+ ax,
298
+ orient="top",
299
+ spacing=None,
300
+ add_meta=True,
301
+ add_base=True,
302
+ base_colors=None,
303
+ meta_color=None,
304
+ linewidth=None,
305
+ divide=True,
306
+ divide_style="--",
307
+ meta_ratio=0.2,
308
+ ):
290
309
  """
291
310
 
292
311
  Parameters
@@ -315,7 +334,7 @@ class GroupDendrogram(_DendrogramBase):
315
334
  """
316
335
 
317
336
  meta_color = ".1" if meta_color is None else meta_color
318
- linewidth = .7 if linewidth is None else linewidth
337
+ linewidth = 0.7 if linewidth is None else linewidth
319
338
  if base_colors is None:
320
339
  base_colors = cycle([None])
321
340
  elif is_color_like(base_colors):
@@ -332,8 +351,7 @@ class GroupDendrogram(_DendrogramBase):
332
351
 
333
352
  render_xlim = self.den_xlim / (1 - np.sum(spacing))
334
353
  skeleton = np.sort(np.unique(self.x_coords[self.y_coords == 0]))
335
- ranger = [(skeleton[i], skeleton[i + 1]) \
336
- for i in range(len(skeleton) - 1)]
354
+ ranger = [(skeleton[i], skeleton[i + 1]) for i in range(len(skeleton) - 1)]
337
355
 
338
356
  draw_dens = self.dens if add_meta else self.orig_dens
339
357
 
@@ -399,28 +417,45 @@ class GroupDendrogram(_DendrogramBase):
399
417
 
400
418
  if add_meta:
401
419
  # Add meta dendrogram
402
- self._draw_dendrogram(ax, orient=orient,
403
- color=meta_color, linewidth=linewidth)
420
+ self._draw_dendrogram(
421
+ ax, orient=orient, color=meta_color, linewidth=linewidth
422
+ )
404
423
 
405
424
  if divide & add_base & add_meta:
406
425
  xmin = np.min(draw_dens[0].x_coords)
407
426
  xmax = np.max(draw_dens[-1]._render_x_coords)
408
427
  if orient in ["top", "bottom"]:
409
- ax.hlines(self.divider, xmin, xmax, # 0, xlim,
410
- linestyles=divide_style, color=meta_color,
411
- linewidth=linewidth)
428
+ ax.hlines(
429
+ self.divider,
430
+ xmin,
431
+ xmax, # 0, xlim,
432
+ linestyles=divide_style,
433
+ color=meta_color,
434
+ linewidth=linewidth,
435
+ )
412
436
  else:
413
- ax.vlines(self.divider, xmin, xmax, # 0, ylim,
414
- linestyles=divide_style, color=meta_color,
415
- linewidth=linewidth)
437
+ ax.vlines(
438
+ self.divider,
439
+ xmin,
440
+ xmax, # 0, ylim,
441
+ linestyles=divide_style,
442
+ color=meta_color,
443
+ linewidth=linewidth,
444
+ )
416
445
 
417
446
  if add_base:
418
447
  for den, color in zip(draw_dens, base_colors):
419
448
  # The singleton dendrogram will only be drawn if meta is drawn
420
449
  if not den.is_singleton or add_meta:
421
- den.draw(ax, orient=orient, add_root=add_meta, color=color,
422
- linewidth=linewidth,
423
- root_color=meta_color, control_ax=False)
450
+ den.draw(
451
+ ax,
452
+ orient=orient,
453
+ add_root=add_meta,
454
+ color=color,
455
+ linewidth=linewidth,
456
+ root_color=meta_color,
457
+ control_ax=False,
458
+ )
424
459
 
425
460
  xlim = render_xlim
426
461
  # reserve room to avoid clipping of the top
marsilea/exceptions.py CHANGED
@@ -1,5 +1,4 @@
1
1
  class DuplicateName(Exception):
2
-
3
2
  def __init__(self, name):
4
3
  self.name = name
5
4
 
@@ -8,7 +7,6 @@ class DuplicateName(Exception):
8
7
 
9
8
 
10
9
  class SplitTwice(Exception):
11
-
12
10
  def __init__(self, axis="col"):
13
11
  self.axis = axis
14
12
 
@@ -21,11 +19,12 @@ class SplitConflict(Exception):
21
19
 
22
20
 
23
21
  class AppendLayoutError(Exception):
24
-
25
22
  def __str__(self):
26
- return "Append a concatenated plot is not allowed," \
27
- "you can only append " \
28
- "plots to a concatenated plot."
23
+ return (
24
+ "Append a concatenated plot is not allowed,"
25
+ "you can only append "
26
+ "plots to a concatenated plot."
27
+ )
29
28
 
30
29
 
31
30
  class DataError(Exception):
marsilea/heatmap.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import numpy as np
5
+ import pandas as pd
5
6
 
6
7
  from .base import ClusterBoard
7
8
  from .plotter import ColorMesh, SizedMesh, Colors
@@ -27,22 +28,54 @@ class Heatmap(ClusterBoard):
27
28
 
28
29
  """
29
30
 
30
- def __init__(self, data: np.ndarray, vmin=None, vmax=None,
31
- cmap=None, norm=None, center=None,
32
- mask=None, alpha=None, linewidth=0, linecolor="white",
33
- annot=None, fmt=None, annot_kws=None, label=None,
34
- cbar_kws=None, name=None,
35
- width=None, height=None, cluster_data=None
36
- ):
31
+ def __init__(
32
+ self,
33
+ data: np.ndarray,
34
+ vmin=None,
35
+ vmax=None,
36
+ cmap=None,
37
+ norm=None,
38
+ center=None,
39
+ mask=None,
40
+ alpha=None,
41
+ linewidth=0,
42
+ linecolor="white",
43
+ annot=None,
44
+ fmt=None,
45
+ annot_kws=None,
46
+ label=None,
47
+ cbar_kws=None,
48
+ name=None,
49
+ width=None,
50
+ height=None,
51
+ cluster_data=None,
52
+ init_main=True,
53
+ ):
37
54
  if cluster_data is None:
38
- cluster_data = data
39
- super().__init__(cluster_data, width=width, height=height,
40
- name=name)
41
- mesh = ColorMesh(data, vmin=vmin, vmax=vmax, cmap=cmap,
42
- norm=norm, center=center,
43
- mask=mask, alpha=alpha, linewidth=linewidth,
44
- linecolor=linecolor, annot=annot, fmt=fmt,
45
- annot_kws=annot_kws, label=label, cbar_kws=cbar_kws)
55
+ if isinstance(data, pd.DataFrame):
56
+ cluster_data = data.values
57
+ else:
58
+ cluster_data = data
59
+ super().__init__(
60
+ cluster_data, width=width, height=height, name=name, init_main=init_main
61
+ )
62
+ mesh = ColorMesh(
63
+ data,
64
+ vmin=vmin,
65
+ vmax=vmax,
66
+ cmap=cmap,
67
+ norm=norm,
68
+ center=center,
69
+ mask=mask,
70
+ alpha=alpha,
71
+ linewidth=linewidth,
72
+ linecolor=linecolor,
73
+ annot=annot,
74
+ fmt=fmt,
75
+ annot_kws=annot_kws,
76
+ label=label,
77
+ cbar_kws=cbar_kws,
78
+ )
46
79
  name = get_plot_name(name, "main", mesh.__class__.__name__)
47
80
  mesh.set(name=name)
48
81
  self.add_layer(mesh)
@@ -65,12 +98,29 @@ class CatHeatmap(ClusterBoard):
65
98
 
66
99
  """
67
100
 
68
- def __init__(self, data, palette=None, cmap=None, mask=None,
69
- name=None, width=None, height=None, cluster_data=None,
70
- linewidth=0, linecolor="white",
71
- **kwargs):
72
- mesh = Colors(data, palette=palette, cmap=cmap, mask=mask,
73
- linewidth=linewidth, linecolor=linecolor, **kwargs)
101
+ def __init__(
102
+ self,
103
+ data,
104
+ palette=None,
105
+ cmap=None,
106
+ mask=None,
107
+ name=None,
108
+ width=None,
109
+ height=None,
110
+ cluster_data=None,
111
+ linewidth=0,
112
+ linecolor="white",
113
+ **kwargs,
114
+ ):
115
+ mesh = Colors(
116
+ data,
117
+ palette=palette,
118
+ cmap=cmap,
119
+ mask=mask,
120
+ linewidth=linewidth,
121
+ linecolor=linecolor,
122
+ **kwargs,
123
+ )
74
124
  if cluster_data is None:
75
125
  cluster_data = mesh.cluster_data
76
126
  super().__init__(cluster_data, width=width, height=height, name=name)
@@ -96,11 +146,21 @@ class SizedHeatmap(ClusterBoard):
96
146
 
97
147
  """
98
148
 
99
- def __init__(self, size, color=None, cluster_data=None,
100
- name=None, width=None, height=None,
101
- **kwargs):
149
+ def __init__(
150
+ self,
151
+ size,
152
+ color=None,
153
+ cluster_data=None,
154
+ name=None,
155
+ width=None,
156
+ height=None,
157
+ **kwargs,
158
+ ):
102
159
  if cluster_data is None:
103
- cluster_data = size
160
+ if isinstance(size, pd.DataFrame):
161
+ cluster_data = size.values
162
+ else:
163
+ cluster_data = size
104
164
  super().__init__(cluster_data, width=width, height=height, name=name)
105
165
 
106
166
  mesh = SizedMesh(size=size, color=color, **kwargs)