d3blocks 2.3.0__tar.gz → 2.3.1__tar.gz

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.
Files changed (172) hide show
  1. {d3blocks-2.3.0/d3blocks.egg-info → d3blocks-2.3.1}/PKG-INFO +1 -1
  2. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/__init__.py +1 -1
  3. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/d3blocks.py +12 -9
  4. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/movingbubbles/Movingbubbles.py +76 -18
  5. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/movingbubbles/d3js/movingbubbles.html.j2 +68 -14
  6. d3blocks-2.3.1/d3blocks/tests/test_movingbubbles_color.py +93 -0
  7. {d3blocks-2.3.0 → d3blocks-2.3.1/d3blocks.egg-info}/PKG-INFO +1 -1
  8. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks.egg-info/SOURCES.txt +1 -0
  9. {d3blocks-2.3.0 → d3blocks-2.3.1}/LICENSE +0 -0
  10. {d3blocks-2.3.0 → d3blocks-2.3.1}/MANIFEST.in +0 -0
  11. {d3blocks-2.3.0 → d3blocks-2.3.1}/README.md +0 -0
  12. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/_shared/__init__.py +0 -0
  13. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/_shared/d3js/__init__.py +0 -0
  14. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/_shared/d3js/d3.v7.min.js +0 -0
  15. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/_shared/d3js/theme.js +0 -0
  16. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/_shared/d3js/walkthrough.css +0 -0
  17. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/_shared/d3js/walkthrough.js +0 -0
  18. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/chord/Chord.py +0 -0
  19. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/chord/__init__.py +0 -0
  20. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/chord/d3js/__init__.py +0 -0
  21. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/chord/d3js/chord.css +0 -0
  22. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/chord/d3js/chord.html.j2 +0 -0
  23. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/chord/d3js/chord.js +0 -0
  24. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/circlepacking/Circlepacking.py +0 -0
  25. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/circlepacking/__init__.py +0 -0
  26. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/circlepacking/d3js/__init__.py +0 -0
  27. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/circlepacking/d3js/circlepacking.css +0 -0
  28. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/circlepacking/d3js/circlepacking.html.j2 +0 -0
  29. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/circlepacking/d3js/d3.v6.min.js +0 -0
  30. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/circlepacking/d3js/lodash.min.js +0 -0
  31. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/__init__.py +0 -0
  32. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/d3js/.gitkeep +0 -0
  33. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/d3js/__init__.py +0 -0
  34. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/d3js/d3.v2.js +0 -0
  35. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/d3js/elasticgraph.css +0 -0
  36. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/d3js/elasticgraph.html.j2 +0 -0
  37. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/d3js/elasticgraph.js +0 -0
  38. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/elasticgraph/elasticgraph.py +0 -0
  39. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/Erd.py +0 -0
  40. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/__init__.py +0 -0
  41. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/d3js/__init__.py +0 -0
  42. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/d3js/d3.erd.js +0 -0
  43. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/d3js/dagre.LICENSE +0 -0
  44. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/d3js/dagre.min.js +0 -0
  45. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/d3js/erd.css +0 -0
  46. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/erd/d3js/erd.html.j2 +0 -0
  47. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/examples.py +0 -0
  48. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/examples_docs.py +0 -0
  49. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/Flowgraph.py +0 -0
  50. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/__init__.py +0 -0
  51. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/__init__.py +0 -0
  52. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/d3.flowgraph.js +0 -0
  53. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/d3.v7.min.js +0 -0
  54. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/dagre.LICENSE +0 -0
  55. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/dagre.min.js +0 -0
  56. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/elk.LICENSE.md +0 -0
  57. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/elk.bundled.js +0 -0
  58. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/flowgraph.css +0 -0
  59. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/flowgraph/d3js/flowgraph.html.j2 +0 -0
  60. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/Heatmap.py +0 -0
  61. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/__init__.py +0 -0
  62. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/d3js/__init__.py +0 -0
  63. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/d3js/d3.scale.chromatic.v1.min.js +0 -0
  64. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/d3js/d3.v4.js +0 -0
  65. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/d3js/heatmap.css +0 -0
  66. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/d3js/heatmap.html.j2 +0 -0
  67. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/heatmap/d3js/matrix.html.j2 +0 -0
  68. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/Imageslider.py +0 -0
  69. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/__init__.py +0 -0
  70. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/.keep +0 -0
  71. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/__init__.py +0 -0
  72. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/imageslider.css +0 -0
  73. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/imageslider.html.j2 +0 -0
  74. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/imageslider.js +0 -0
  75. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/jquery-2.1.1.js +0 -0
  76. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/modernizr.js +0 -0
  77. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/imageslider/d3js/reset.css +0 -0
  78. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/logo.txt +0 -0
  79. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/Maps.py +0 -0
  80. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/__init__.py +0 -0
  81. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/d3js/__init__.py +0 -0
  82. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/d3js/d3.v6.min.js +0 -0
  83. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/d3js/geojson.zip +0 -0
  84. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/d3js/maps.css +0 -0
  85. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/d3js/maps.html.j2 +0 -0
  86. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/maps/d3js/world.geojson +0 -0
  87. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/matrix/Matrix.py +0 -0
  88. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/matrix/__init__.py +0 -0
  89. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/matrix/d3js/__init__.py +0 -0
  90. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/matrix/d3js/d3.scale.chromatic.v1.min.js +0 -0
  91. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/matrix/d3js/d3.v4.js +0 -0
  92. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/matrix/d3js/matrix.html.j2 +0 -0
  93. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/movingbubbles/__init__.py +0 -0
  94. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/movingbubbles/d3js/__init__.py +0 -0
  95. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/movingbubbles/d3js/d3-3-5-5.min.js +0 -0
  96. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/movingbubbles/d3js/movingbubbles.css +0 -0
  97. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/particles/Particles.py +0 -0
  98. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/particles/__init__.py +0 -0
  99. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/particles/d3js/__init__.py +0 -0
  100. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/particles/d3js/d3-scale-chromatic.v1.min.js +0 -0
  101. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/particles/d3js/d3.v4.min.js +0 -0
  102. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/particles/d3js/particles.html.j2 +0 -0
  103. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/radialgraph/Radialgraph.py +0 -0
  104. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/radialgraph/__init__.py +0 -0
  105. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/radialgraph/d3js/__init__ .py +0 -0
  106. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/radialgraph/d3js/d3.radialgraph.js +0 -0
  107. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/radialgraph/d3js/d3.v7.min.js +0 -0
  108. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/radialgraph/d3js/radialgraph.css +0 -0
  109. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/radialgraph/d3js/radialgraph.html.j2 +0 -0
  110. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sankey/Sankey.py +0 -0
  111. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sankey/__init__.py +0 -0
  112. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sankey/d3js/__init__.py +0 -0
  113. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sankey/d3js/sankey.css +0 -0
  114. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sankey/d3js/sankey.html.j2 +0 -0
  115. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sankey/d3js/sankey.js +0 -0
  116. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/scatter/Scatter.py +0 -0
  117. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/scatter/__init__.py +0 -0
  118. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/scatter/d3js/__init__.py +0 -0
  119. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/scatter/d3js/associations.js +0 -0
  120. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/scatter/d3js/d3.v4.min.js +0 -0
  121. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/scatter/d3js/scatter.css +0 -0
  122. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/scatter/d3js/scatter.html.j2 +0 -0
  123. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sequence/Sequence.py +0 -0
  124. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sequence/__init__.py +0 -0
  125. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sequence/d3js/__init__.py +0 -0
  126. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sequence/d3js/d3.sequence.js +0 -0
  127. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sequence/d3js/sequence.css +0 -0
  128. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/sequence/d3js/sequence.html.j2 +0 -0
  129. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/__init__.py +0 -0
  130. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_all_examples.py +0 -0
  131. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_chord_margins.py +0 -0
  132. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_d3blocks.py +0 -0
  133. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_erd.py +0 -0
  134. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_flowgraph.py +0 -0
  135. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_medium_charts.py +0 -0
  136. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_movingbubbles_timedelta.py +0 -0
  137. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_radialgraph.py +0 -0
  138. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tests/test_sequence.py +0 -0
  139. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/timeseries/Timeseries.py +0 -0
  140. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/timeseries/__init__.py +0 -0
  141. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/timeseries/d3js/__init__.py +0 -0
  142. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/timeseries/d3js/d3.v3.js +0 -0
  143. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/timeseries/d3js/timeseries.css +0 -0
  144. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/timeseries/d3js/timeseries.html.j2 +0 -0
  145. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/timeseries/d3js/timeseries.js +0 -0
  146. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/Tree.py +0 -0
  147. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/__init__.py +0 -0
  148. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/d3js/__init__.py +0 -0
  149. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/d3js/collapsibleTree.js +0 -0
  150. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/d3js/d3.tree.js +0 -0
  151. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/d3js/d3.v4.min.js +0 -0
  152. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/d3js/tree.css +0 -0
  153. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/tree/d3js/tree.html.j2 +0 -0
  154. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/treemap/Treemap.py +0 -0
  155. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/treemap/__init__.py +0 -0
  156. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/treemap/d3js/__init__.py +0 -0
  157. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/treemap/d3js/d3.v4.min.js +0 -0
  158. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/treemap/d3js/treemap.css +0 -0
  159. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/treemap/d3js/treemap.html.j2 +0 -0
  160. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/utils.py +0 -0
  161. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/violin/Violin.py +0 -0
  162. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/violin/__init__.py +0 -0
  163. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/violin/d3js/__init__.py +0 -0
  164. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/violin/d3js/d3-scale-chromatic.v1.min.js +0 -0
  165. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/violin/d3js/d3.v4.min.js +0 -0
  166. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/violin/d3js/violin.css +0 -0
  167. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks/violin/d3js/violin.html.j2 +0 -0
  168. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks.egg-info/dependency_links.txt +0 -0
  169. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks.egg-info/requires.txt +0 -0
  170. {d3blocks-2.3.0 → d3blocks-2.3.1}/d3blocks.egg-info/top_level.txt +0 -0
  171. {d3blocks-2.3.0 → d3blocks-2.3.1}/pyproject.toml +0 -0
  172. {d3blocks-2.3.0 → d3blocks-2.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3blocks
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Python package d3blocks.
5
5
  Author-email: Erdogan Taskesen <erdogant@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -10,7 +10,7 @@ from d3blocks.utils import (
10
10
 
11
11
  __author__ = 'Erdogan Taskesen'
12
12
  __email__ = 'erdogant@gmail.com'
13
- __version__ = '2.3.0'
13
+ __version__ = '2.3.1'
14
14
 
15
15
  # Setup root logger
16
16
  import logging
@@ -1386,7 +1386,7 @@ class D3Blocks():
1386
1386
  opacity: float = 0.6,
1387
1387
  stroke: str = '#000000',
1388
1388
  cmap: str = 'Set1',
1389
- color_method: str = 'state',
1389
+ color_method: str = 'auto',
1390
1390
  dt_format: str = '%d-%m-%Y %H:%M:%S',
1391
1391
  damper: float = 1,
1392
1392
  fontsize: int = 14,
@@ -1440,12 +1440,14 @@ class D3Blocks():
1440
1440
  * [4, 10, 5, ...]: array-like of the same length as ``df``, aligned positionally with its rows (same convention as ``d3.scatter``'s ``size``).
1441
1441
  * {'0': 4, '1': 10, '2': 5, ..}: dict keyed by sample_id value.
1442
1442
 
1443
- color : int, list/array/Series, or dictionary. (default: '#808080') or {sample_id i: hex-color}
1443
+ color : str, list/array/Series, or dictionary. (default: None)
1444
1444
  Color the nodes.
1445
1445
  * '#000FFF': set all nodes to this color.
1446
- * ['#808080', '#FFF000', ...]: array-like of the same length as ``df``, aligned positionally with its rows.
1447
- * {'0': '#808080', '1': '#FFF000', '3': '#000000', ..}: dict keyed by sample_id value.
1448
- * None: Colors are based on sample_id using the cmap.
1446
+ * ['#808080', '#FFF000', ...]: array-like hex colors of the same length as ``df``, aligned positionally with its rows.
1447
+ * ['Netherlands', 'Germany', ...]: array-like category labels of the same length as ``df``; labels are mapped to colors using ``cmap``.
1448
+ * {'0': '#808080', '1': '#FFF000', '3': '#000000', ..}: dict keyed by sample_id value with hex colors.
1449
+ * {'0': 'NL', '1': 'DE', ..}: dict keyed by sample_id value with category labels; labels are mapped to colors using ``cmap``.
1450
+ * None: Colors are based on sample_id using ``cmap``.
1449
1451
  opacity : float, list/array/Series, or dictionary, (default: 0.6)
1450
1452
  Opacity of the bubbles [0-1].
1451
1453
  * 0.6: set all nodes to this opacity.
@@ -1458,9 +1460,10 @@ class D3Blocks():
1458
1460
  * {'0': '#000000', '1': '#ffffff', ..}: dict keyed by sample_id value.
1459
1461
 
1460
1462
  color_method : str
1461
- Coloring of the nodes.
1462
- * 'state': Use the colors defined per state as (d3.node_properties).
1463
- * 'node': Use the colors defined in the dataframe (d3.edge_properties).
1463
+ Coloring mode of the nodes.
1464
+ * 'auto' (default): if ``color`` is provided, use node colors ('node'); otherwise use state colors ('state').
1465
+ * 'state': Use the colors defined per state (``d3.node_properties``), so bubble colors change with activity/state.
1466
+ * 'node': Use the colors defined on nodes (from ``color`` / ``d3.edge_properties``), so each sample keeps its assigned color.
1464
1467
  dt_format : str
1465
1468
  Date time format.
1466
1469
  * '%d-%m-%Y %H:%M:%S'.
@@ -1634,7 +1637,7 @@ class D3Blocks():
1634
1637
  # Store chart
1635
1638
  self.chart = set_chart_func('Movingbubbles', logger)
1636
1639
  # Store properties
1637
- self.config = self.chart.set_config(config=self.config, filepath=filepath, title=title, showfig=showfig, overwrite=overwrite, figsize=figsize, timedelta=timedelta, speed=speed, damper=damper, note=note, time_notes=time_notes, fontsize=fontsize, standardize=standardize, center=center, datetime=datetime, sample_id=sample_id, state=state, reset_properties=reset_properties, cmap=cmap, dt_format=dt_format, notebook=notebook, color_method=color_method, save_button=save_button, show_side_panel=show_side_panel, show_top_panel=show_top_panel, dark_mode=dark_mode, color_background=color_background, node_text_inside=node_text_inside, opacity=opacity, stroke=stroke, logger=logger)
1640
+ self.config = self.chart.set_config(config=self.config, filepath=filepath, title=title, showfig=showfig, overwrite=overwrite, figsize=figsize, timedelta=timedelta, speed=speed, damper=damper, note=note, time_notes=time_notes, fontsize=fontsize, standardize=standardize, center=center, datetime=datetime, sample_id=sample_id, state=state, reset_properties=reset_properties, cmap=cmap, dt_format=dt_format, notebook=notebook, color_method=color_method, color=color, save_button=save_button, show_side_panel=show_side_panel, show_top_panel=show_top_panel, dark_mode=dark_mode, color_background=color_background, node_text_inside=node_text_inside, opacity=opacity, stroke=stroke, logger=logger)
1638
1641
  # Set node properties
1639
1642
  if self.config['reset_properties'] or (not hasattr(self, 'node_properties')):
1640
1643
  self.set_node_properties(df[self.config['state']].values, center=self.config['center'], cmap=self.config['cmap'], logger=logger)
@@ -24,6 +24,25 @@ except:
24
24
  from utils import convert_dataframe_dict, set_path, pre_processing, update_config, write_html_file, include_save_to_svg_script, set_logo, resolve_color_background
25
25
 
26
26
 
27
+ def _resolve_color_method(color_method, color_provided=False, logger=None):
28
+ """Resolve color_method to one of {'STATE','NODE'}.
29
+
30
+ AUTO resolves to NODE when user-supplied colors are provided, otherwise
31
+ to STATE so state/activity colors remain the default behavior.
32
+ """
33
+ cm = str('AUTO' if color_method is None else color_method).upper()
34
+ if cm == 'AUTO':
35
+ resolved = 'NODE' if color_provided else 'STATE'
36
+ if logger is not None:
37
+ logger.info('[Movingbubbles] color_method="auto" resolved to [%s].' % (resolved.lower()))
38
+ return resolved
39
+ if cm not in {'STATE', 'NODE'}:
40
+ if logger is not None:
41
+ logger.warning('[Movingbubbles] Unknown color_method [%s]. Falling back to [state].' % (cm))
42
+ return 'STATE'
43
+ return cm
44
+
45
+
27
46
  # %% Set configuration properties
28
47
  def set_config(config={}, **kwargs):
29
48
  """Set the default configuration settings."""
@@ -50,7 +69,12 @@ def set_config(config={}, **kwargs):
50
69
  config['dt_format'] = kwargs.get('dt_format', '%d-%m-%Y %H:%M:%S')
51
70
  config['columns'] = kwargs.get('columns', {'datetime': config['datetime'], 'sample_id': config['sample_id'], 'state': config['state']})
52
71
  config['notebook'] = kwargs.get('notebook', False)
53
- config['color_method'] = kwargs.get('color_method', "STATE")
72
+ config['color_provided'] = kwargs.get('color_provided', kwargs.get('color', None) is not None)
73
+ config['color_method'] = _resolve_color_method(
74
+ kwargs.get('color_method', 'AUTO'),
75
+ color_provided=config['color_provided'],
76
+ logger=logger,
77
+ )
54
78
  config['save_button'] = kwargs.get('save_button', True)
55
79
  config['show_side_panel'] = kwargs.get('show_side_panel', True)
56
80
  config['show_top_panel'] = kwargs.get('show_top_panel', True)
@@ -173,7 +197,8 @@ def set_edge_properties(df, **kwargs):
173
197
  color = kwargs.get('color', None)
174
198
  opacity = kwargs.get('opacity', 0.6)
175
199
  stroke = kwargs.get('stroke', '#000000')
176
- cmap = kwargs.get('cmap', 'Set1')
200
+ config = kwargs.get('config', {})
201
+ cmap = kwargs.get('cmap', config.get('cmap', 'Set1'))
177
202
  dt_format = kwargs.get('dt_format', '%d-%m-%Y %H:%M:%S')
178
203
  logger = kwargs.get('logger', None)
179
204
  df = df.copy()
@@ -205,28 +230,46 @@ def _array_like_len_check(name, values, n):
205
230
  raise Exception('[Movingbubbles] Input parameter [%s] as an array/list/Series must have the same length as the input dataframe: expected %d, got %d.' % (name, n, len(values)))
206
231
 
207
232
 
233
+ def _is_hex_color(value):
234
+ return isinstance(value, str) and bool(re.fullmatch(r'#[0-9a-fA-F]{6}', value))
235
+
236
+
237
+ def _labels_to_hex(labels, cmap):
238
+ labels = pd.Series(labels, copy=False).astype(str).values
239
+ colors = colourmap.fromlist(labels, cmap=cmap, scheme='hex', method='matplotlib', verbose=0)[0]
240
+ return [str(c) for c in colors]
241
+
242
+
208
243
  def _set_nodecolor(df, sample_id, color, cmap, logger):
209
244
  if isinstance(color, dict):
210
- # Per-node colors supplied as {sample_id: hex}
245
+ # Per-node colors supplied as {sample_id: color}. Values can be hex
246
+ # colors or categorical labels that are mapped via cmap.
211
247
  if logger is not None: logger.info('Processing the specified node colors as a dictionary..')
212
248
  df['color'] = '#808080'
213
- for key in color.keys():
214
- df.loc[df[sample_id]==key, 'color'] = str(color.get(key))
249
+ keys = list(color.keys())
250
+ vals = list(color.values())
251
+ if len(vals) > 0 and not np.all([_is_hex_color(v) for v in vals]):
252
+ vals = _labels_to_hex(vals, cmap)
253
+ for key, val in zip(keys, vals):
254
+ df.loc[df[sample_id] == key, 'color'] = str(val)
215
255
  elif isinstance(color, (list, tuple, np.ndarray, pd.Series)):
216
256
  # Array-like, aligned positionally with the dataframe rows.
217
257
  color_arr = np.asarray(color)
218
258
  _array_like_len_check('color', color_arr, len(df))
219
- if logger is not None: logger.info('Processing the specified node colors as an array aligned with the dataframe..')
220
- df['color'] = [str(c) for c in color_arr]
259
+ if np.all([_is_hex_color(v) for v in color_arr]):
260
+ if logger is not None: logger.info('Processing the specified node colors as hex colors aligned with the dataframe..')
261
+ df['color'] = [str(c) for c in color_arr]
262
+ else:
263
+ if logger is not None: logger.info('Processing the specified node colors as labels aligned with the dataframe and mapping via cmap..')
264
+ df['color'] = _labels_to_hex(color_arr, cmap)
221
265
  elif color is None:
222
266
  # Derive colors from cmap based on sample_id
223
- colors = colourmap.fromlist(df[sample_id], cmap=cmap, scheme='hex')[0]
224
- df['color'] = [str(c) for c in colors]
267
+ df['color'] = _labels_to_hex(df[sample_id], cmap)
225
268
  else:
226
269
  # Single hex color string applied to all nodes
227
270
  df['color'] = str(color)
228
271
  if logger is not None: logger.info('Set all nodes to color: %s' %(color))
229
-
272
+
230
273
  return df
231
274
 
232
275
 
@@ -331,6 +374,10 @@ def show(df, **kwargs):
331
374
  sid_col = config['columns']['sample_id']
332
375
  sid = np.array(list(map(lambda x: labels.get(x)['id'], df[config['columns']['state']].values)))
333
376
  uiid = np.unique(df[sid_col])
377
+ # size/opacity schedules: one list per sample, aligned with that sample's
378
+ # activity steps so the frontend can update radius/opacity on state change.
379
+ node_size_sched = []
380
+ node_opacity_sched = []
334
381
  for i in uiid:
335
382
  # Combine the sample_id with its time in state
336
383
  Iloc=df[sid_col]==i
@@ -346,19 +393,25 @@ def show(df, **kwargs):
346
393
  # Make one big happy list
347
394
  X.append(tmplist)
348
395
 
349
- # Node size in the same order as the uiid
350
- nodedict = dict(zip(df[sid_col], df['size']))
351
- config['node_size'] = [int(nodedict.get(x)) if isinstance(nodedict.get(x), (np.integer, int)) else nodedict.get(x) for x in uiid]
396
+ size_vals = df['size'].loc[Iloc].values
397
+ node_size_sched.append([
398
+ int(v) if isinstance(v, (np.integer, int)) else float(v)
399
+ for v in size_vals
400
+ ])
401
+ opacity_vals = df['opacity'].loc[Iloc].values
402
+ node_opacity_sched.append([float(v) for v in opacity_vals])
403
+
404
+ # Per-sample size/opacity schedules (list of lists, same order as uiid /
405
+ # schedule steps). Frontend uses index 0 at start and updates on each
406
+ # activity transition so size/opacity track the current state.
407
+ config['node_size'] = node_size_sched
408
+ config['node_opacity'] = node_opacity_sched
352
409
 
353
410
  # Node color in the same order as the uiid
354
411
  nodedict = dict(zip(df[sid_col], df['color']))
355
412
  # Convert NumPy strings to regular Python strings for proper JSON serialization
356
413
  config['node_color'] = [str(nodedict.get(x)) if nodedict.get(x) is not None else nodedict.get(x) for x in uiid]
357
414
 
358
- # Node opacity in the same order as the uiid
359
- nodedict = dict(zip(df[sid_col], df['opacity']))
360
- config['node_opacity'] = [float(nodedict.get(x)) for x in uiid]
361
-
362
415
  # Node stroke color in the same order as the uiid
363
416
  nodedict = dict(zip(df[sid_col], df['stroke']))
364
417
  config['node_stroke'] = [str(nodedict.get(x)) if nodedict.get(x) is not None else nodedict.get(x) for x in uiid]
@@ -523,7 +576,11 @@ def write_html(X, config, logger=None):
523
576
  zero_to_min = "0" if config['start_minute']<10 else ""
524
577
 
525
578
  # Set the selectionbox correctly on the form
526
- config['color_method'] = config['color_method'].upper()
579
+ config['color_method'] = _resolve_color_method(
580
+ config.get('color_method', 'AUTO'),
581
+ color_provided=config.get('color_provided', False),
582
+ logger=logger,
583
+ )
527
584
  SELECTED_STATE = {'STATE': '', 'NODE': ''}
528
585
  SELECTED_STATE[config['color_method']] = 'selected="selected"'
529
586
  bg_dark, bg_light = resolve_color_background(config.get('color_background'))
@@ -553,6 +610,7 @@ def write_html(X, config, logger=None):
553
610
  'NOTE': config['note'],
554
611
  'TIME_NOTES': config['time_notes'],
555
612
  'COLOR_METHOD': config['color_method'],
613
+ 'COLOR_PROVIDED': 'true' if config.get('color_provided', False) else 'false',
556
614
  'START_HOUR_MIN': config['start_hour'] + (config['start_minute'] / 60),
557
615
  'START_TIME': zero_to_hour + str(config['start_hour']) + ":" + zero_to_min + str(config['start_minute']),
558
616
  'TIMEDELTA': config['timedelta'],
@@ -260,9 +260,23 @@ var notes_index = 0;
260
260
  var colorByActivity = {{ COLORBYACTIVITY }} // array of the colors per state
261
261
 
262
262
  // NODE PROPERTIES
263
- var node_size = {{ NODE_SIZE }}; //array of radius values, one for each node
263
+ var node_size = {{ NODE_SIZE }}; // per-node size schedule: list of lists (one value per activity step), or legacy flat array
264
264
  var node_color = {{ NODE_COLOR }}; //array of color values, one for each node
265
- var node_opacity = {{ NODE_OPACITY }}; //array of opacity values, one for each node
265
+ var node_opacity = {{ NODE_OPACITY }}; // per-node opacity schedule: list of lists (one value per activity step), or legacy flat array
266
+
267
+ // Resolve size/opacity for node i at schedule step idx.
268
+ // Supports both schedule arrays (list-of-lists from Python) and legacy
269
+ // flat arrays / scalars (one value per node).
270
+ function sizeAt(i, idx) {
271
+ var s = node_size[i];
272
+ if (Array.isArray(s)) return +s[Math.min(idx, s.length - 1)];
273
+ return +s;
274
+ }
275
+ function opacityAt(i, idx) {
276
+ var o = node_opacity[i];
277
+ if (Array.isArray(o)) return +o[Math.min(idx, o.length - 1)];
278
+ return +o;
279
+ }
266
280
  var node_stroke = {{ NODE_STROKE }}; //array of stroke colors, one for each node
267
281
  var nodeTextInside = {{ nodeTextInside }}; //true: labels centered inside the node; false: labels above it
268
282
 
@@ -283,10 +297,15 @@ var PROP_KEYS = {{ PROP_KEYS }}; // ordered list of all selectable variables
283
297
  var STATE_COL = {{ STATE_COL }}; // name of the state column (live-updated)
284
298
  var SAMPLE_ID_COL = {{ SAMPLE_ID_COL }};
285
299
  var color_method = "{{ COLOR_METHOD }}"; //Nodes can be colored based on "STATE" or "NODE"
300
+ var color_provided = {{ COLOR_PROVIDED }};
301
+ var COLOR_MODE_STATE = "__mode_state__";
302
+ var COLOR_MODE_CUSTOM = "__mode_custom__";
303
+ var customColorLabel = color_provided ? "custom" : "node";
286
304
  // Layout-panel overrides (null = use defaults / color_method)
287
305
  var layoutSizeKey = null;
288
306
  var layoutColorKey = null;
289
307
  var layoutColorScale = null; // function(value) -> color, when layoutColorKey is set
308
+ var colorModeInitialized = false;
290
309
  var currentShapeMode = "circle"; // circle|square|triangle|diamond
291
310
  var categoricalShapeMap = null;
292
311
  var categoricalShapeKey = null;
@@ -420,13 +439,14 @@ var nodes = sched_objs.map(function(o, i) {
420
439
  act: act,
421
440
  x: init_x,
422
441
  y: init_y,
423
- radius: node_size[i], //use the i-th value from the node_size array to set the radius of the i-th node
442
+ radius: sizeAt(i, 0),
424
443
  color: (color_method === "STATE" ? color(act) : node_color[i]),
425
- opacity: node_opacity[i],
444
+ opacity: opacityAt(i, 0),
426
445
  stroke: node_stroke[i],
427
446
  moves: 0,
428
447
  next_move_time: o[0].duration,
429
448
  sched: o,
449
+ nodeIndex: i,
430
450
  props: (node_props && node_props[i]) ? node_props[i] : {},
431
451
  shape: "circle",
432
452
  }
@@ -730,11 +750,18 @@ function timer() {
730
750
  curr_node.cx = foci[curr_node.act].x;
731
751
  curr_node.cy = foci[curr_node.act].y;
732
752
 
753
+ // Update size/opacity for the new state (schedule-aligned).
754
+ // baseNodeRadii drives recomputeRadii() so size multiplier /
755
+ // layout size-by-variable still apply on top.
756
+ baseNodeRadii[i] = sizeAt(i, curr_moves);
757
+ curr_node.opacity = opacityAt(i, curr_moves);
758
+
733
759
  nodes[i].next_move_time += nodes[i].sched[ curr_node.moves ].duration;
734
760
  }
735
761
 
736
762
  });
737
763
 
764
+ if (typeof recomputeRadii === 'function') recomputeRadii();
738
765
  force.resume();
739
766
  curr_minute += 1;
740
767
 
@@ -817,9 +844,13 @@ function tick(e) {
817
844
  //}
818
845
 
819
846
  // Base color from state/node, unless Layout→Color overrides it.
820
- // Special case: coloring by the state column uses the *live* activity.
821
- if (layoutColorKey && layoutColorKey === STATE_COL) {
847
+ // Special cases:
848
+ // - COLOR_MODE_STATE: live activity colors
849
+ // - COLOR_MODE_CUSTOM: original node colors (custom/node)
850
+ if (layoutColorKey === COLOR_MODE_STATE || layoutColorKey === STATE_COL) {
822
851
  o.color = color(curr_act);
852
+ } else if (layoutColorKey === COLOR_MODE_CUSTOM) {
853
+ o.color = node_color[i];
823
854
  } else if (layoutColorKey && layoutColorScale && o.props) {
824
855
  var pv = o.props[layoutColorKey];
825
856
  o.color = (pv != null && pv !== '') ? layoutColorScale(pv) : (color_method === "STATE" ? color(curr_act) : node_color[i]);
@@ -836,6 +867,7 @@ function tick(e) {
836
867
  circle
837
868
  .each(collide(.5))
838
869
  .style("fill", function(d) { return d.color; })
870
+ .style("opacity", function(d) { return d.opacity; })
839
871
  .attr("d", function(d) { return shapePath(shapeForNode(d), d.radius); })
840
872
  .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
841
873
 
@@ -945,15 +977,18 @@ function resetSimulation(keepSpeed) {
945
977
  for (k in act_counts) {
946
978
  if (Object.prototype.hasOwnProperty.call(act_counts, k)) act_counts[k] = 0;
947
979
  }
948
- nodes.forEach(function (n) {
980
+ nodes.forEach(function (n, i) {
949
981
  var act = n.sched[0].act;
950
982
  n.act = act;
951
983
  n.moves = 0;
952
984
  n.next_move_time = n.sched[0].duration;
953
985
  n.x = foci[act].x + Math.random();
954
986
  n.y = foci[act].y + Math.random();
987
+ baseNodeRadii[i] = sizeAt(i, 0);
988
+ n.opacity = opacityAt(i, 0);
955
989
  act_counts[act] += 1;
956
990
  });
991
+ if (typeof recomputeRadii === 'function') recomputeRadii();
957
992
 
958
993
  // Clear selection (trails are intentionally left alone -- only the
959
994
  // "Clear trails" button should remove them; Reset restarts positions
@@ -1289,8 +1324,14 @@ function applyColorMode(key) {
1289
1324
  layoutColorKey = key || null;
1290
1325
  layoutColorScale = null;
1291
1326
  if (!key) return;
1292
- // State column: live activity colors (handled in tick via color()).
1293
- if (key === STATE_COL) return;
1327
+ // Built-in base color modes.
1328
+ if (key === COLOR_MODE_STATE || key === COLOR_MODE_CUSTOM) return;
1329
+ // Backward-compatibility: if state column value is selected, treat it as
1330
+ // live state coloring mode.
1331
+ if (key === STATE_COL) {
1332
+ layoutColorKey = COLOR_MODE_STATE;
1333
+ return;
1334
+ }
1294
1335
  if (isNumericProp(key)) {
1295
1336
  var vals = numericValues(key);
1296
1337
  var min = d3.min(vals), max = d3.max(vals);
@@ -1368,6 +1409,10 @@ function initLayoutPropertyControls() {
1368
1409
 
1369
1410
  var prevSize = sizeSelect ? sizeSelect.value : '';
1370
1411
  var prevColor = colorSelect ? colorSelect.value : '';
1412
+ if (!colorModeInitialized) {
1413
+ prevColor = (color_method === "STATE") ? COLOR_MODE_STATE : COLOR_MODE_CUSTOM;
1414
+ colorModeInitialized = true;
1415
+ }
1371
1416
  var prevShape = shapeSelect ? shapeSelect.value : 'circle';
1372
1417
  var prevLabel = labelSelect ? labelSelect.value : '';
1373
1418
 
@@ -1387,12 +1432,17 @@ function initLayoutPropertyControls() {
1387
1432
  o.textContent = label;
1388
1433
  select.appendChild(o);
1389
1434
  }
1435
+ // Built-in base color modes are always available in the Color dropdown.
1436
+ addOpt(colorSelect, COLOR_MODE_STATE, "state");
1437
+ addOpt(colorSelect, COLOR_MODE_CUSTOM, customColorLabel);
1438
+
1390
1439
  keys.forEach(function (k) {
1391
1440
  if (!isVarEnabled(k)) return;
1392
1441
  // State is always categorical for layout purposes.
1393
1442
  var numeric = (k !== STATE_COL) && isNumericProp(k);
1394
1443
  if (numeric) addOpt(sizeSelect, k, k);
1395
- addOpt(colorSelect, k, k);
1444
+ // STATE_COL has a dedicated built-in mode label "state".
1445
+ if (k !== STATE_COL) addOpt(colorSelect, k, k);
1396
1446
  if (!numeric) addOpt(shapeSelect, "prop:" + k, k);
1397
1447
  addOpt(labelSelect, k, k);
1398
1448
  });
@@ -1409,7 +1459,8 @@ function initLayoutPropertyControls() {
1409
1459
  applySizeMode(sizeSelect.value);
1410
1460
  }
1411
1461
  if (colorSelect) {
1412
- colorSelect.value = hasOption(colorSelect, prevColor) ? prevColor : '';
1462
+ var colorFallback = (color_method === "STATE") ? COLOR_MODE_STATE : COLOR_MODE_CUSTOM;
1463
+ colorSelect.value = hasOption(colorSelect, prevColor) ? prevColor : colorFallback;
1413
1464
  applyColorMode(colorSelect.value);
1414
1465
  }
1415
1466
  if (shapeSelect) {
@@ -1523,11 +1574,11 @@ if (clearTrailsBtn) clearTrailsBtn.addEventListener("click", function () {
1523
1574
  var layoutResetBtn = document.getElementById("layoutResetBtn");
1524
1575
  if (layoutResetBtn) layoutResetBtn.addEventListener("click", function () {
1525
1576
  if (layoutSizeSelect) layoutSizeSelect.value = "";
1526
- if (layoutColorSelect) layoutColorSelect.value = "";
1577
+ if (layoutColorSelect) layoutColorSelect.value = (color_method === "STATE") ? COLOR_MODE_STATE : COLOR_MODE_CUSTOM;
1527
1578
  if (layoutShapeSelect) layoutShapeSelect.value = "circle";
1528
1579
  if (layoutLabelSelect) layoutLabelSelect.value = "";
1529
1580
  applySizeMode("");
1530
- applyColorMode("");
1581
+ applyColorMode(layoutColorSelect ? layoutColorSelect.value : "");
1531
1582
  applyShapeMode("circle");
1532
1583
  applyLabelMode("");
1533
1584
  });
@@ -1595,15 +1646,18 @@ function scrubToMinute(targetMinute) {
1595
1646
  if (Object.prototype.hasOwnProperty.call(act_counts, k)) act_counts[k] = 0;
1596
1647
  }
1597
1648
 
1598
- nodes.forEach(function (n) {
1649
+ nodes.forEach(function (n, i) {
1599
1650
  var s = nodeStateAtMinute(n, targetMinute);
1600
1651
  n.act = s.act;
1601
1652
  n.moves = s.moves;
1602
1653
  n.next_move_time = s.next_move_time;
1654
+ baseNodeRadii[i] = sizeAt(i, s.moves);
1655
+ n.opacity = opacityAt(i, s.moves);
1603
1656
  // Do NOT snap x/y — let the force layout pull each bubble
1604
1657
  // smoothly toward its new focus so nothing teleports or explodes.
1605
1658
  act_counts[n.act] += 1;
1606
1659
  });
1660
+ if (typeof recomputeRadii === 'function') recomputeRadii();
1607
1661
 
1608
1662
  curr_minute = targetMinute;
1609
1663
 
@@ -0,0 +1,93 @@
1
+ import colourmap
2
+ import pandas as pd
3
+
4
+ from d3blocks.movingbubbles.Movingbubbles import set_config, set_edge_properties
5
+
6
+
7
+ def _make_df():
8
+ return pd.DataFrame(
9
+ {
10
+ "row_id": [0, 1, 2, 3],
11
+ "datetime": pd.to_datetime(
12
+ [
13
+ "2023-01-01 00:00:00",
14
+ "2023-01-01 00:01:00",
15
+ "2023-01-01 00:02:00",
16
+ "2023-01-01 00:03:00",
17
+ ]
18
+ ),
19
+ "sample_id": [1, 2, 1, 2],
20
+ "state": ["A", "B", "A", "B"],
21
+ }
22
+ )
23
+
24
+
25
+ def test_movingbubbles_color_series_categories_uses_cmap():
26
+ df = _make_df()
27
+ categories = pd.Series(["Netherlands", "Germany", "Netherlands", "Norway"])
28
+
29
+ out = set_edge_properties(
30
+ df,
31
+ datetime="datetime",
32
+ sample_id="sample_id",
33
+ state="state",
34
+ color=categories,
35
+ standardize="samplewise",
36
+ timedelta="minutes",
37
+ config={"cmap": "Dark2"},
38
+ )
39
+
40
+ expected = [
41
+ str(c)
42
+ for c in colourmap.fromlist(
43
+ categories.astype(str).values,
44
+ cmap="Dark2",
45
+ scheme="hex",
46
+ method="matplotlib",
47
+ verbose=0,
48
+ )[0]
49
+ ]
50
+
51
+ out_by_input_order = out.sort_values("row_id").reset_index(drop=True)
52
+ assert out_by_input_order["color"].tolist() == expected
53
+
54
+
55
+ def test_movingbubbles_color_method_auto_resolves_to_node_when_color_given():
56
+ cfg = set_config(config={}, color_method="auto", color=["A", "B"])
57
+ assert cfg["color_method"] == "NODE"
58
+
59
+
60
+ def test_movingbubbles_color_method_auto_resolves_to_state_without_color():
61
+ cfg = set_config(config={}, color_method="auto", color=None)
62
+ assert cfg["color_method"] == "STATE"
63
+
64
+
65
+ def test_movingbubbles_color_dict_categories_uses_cmap():
66
+ df = _make_df()
67
+ category_by_sample = {1: "group_a", 2: "group_b"}
68
+
69
+ out = set_edge_properties(
70
+ df,
71
+ datetime="datetime",
72
+ sample_id="sample_id",
73
+ state="state",
74
+ color=category_by_sample,
75
+ standardize="samplewise",
76
+ timedelta="minutes",
77
+ cmap="Set2",
78
+ )
79
+
80
+ mapped = [
81
+ str(c)
82
+ for c in colourmap.fromlist(
83
+ list(category_by_sample.values()),
84
+ cmap="Set2",
85
+ scheme="hex",
86
+ method="matplotlib",
87
+ verbose=0,
88
+ )[0]
89
+ ]
90
+ expected_by_row = [mapped[0], mapped[1], mapped[0], mapped[1]]
91
+
92
+ out_by_input_order = out.sort_values("row_id").reset_index(drop=True)
93
+ assert out_by_input_order["color"].tolist() == expected_by_row
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d3blocks
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Python package d3blocks.
5
5
  Author-email: Erdogan Taskesen <erdogant@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -136,6 +136,7 @@ d3blocks/tests/test_d3blocks.py
136
136
  d3blocks/tests/test_erd.py
137
137
  d3blocks/tests/test_flowgraph.py
138
138
  d3blocks/tests/test_medium_charts.py
139
+ d3blocks/tests/test_movingbubbles_color.py
139
140
  d3blocks/tests/test_movingbubbles_timedelta.py
140
141
  d3blocks/tests/test_radialgraph.py
141
142
  d3blocks/tests/test_sequence.py
File without changes
File without changes
File without changes
File without changes