node_info.jsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /**
  2. * Template for node info.
  3. */
  4. import preact from 'preact';
  5. import _ from 'lodash';
  6. const normalCell = {
  7. 'border': 0,
  8. 'border-collapse': 'separate',
  9. 'padding': '2px',
  10. };
  11. /**
  12. * Style definitions which are directly injected (see README.md comments).
  13. */
  14. const style = {
  15. featuresTable: {
  16. 'background-color': 'rgba(255, 255, 255, 0.9)',
  17. 'border': '1px solid #dddddd',
  18. 'border-spacing': '2px',
  19. 'border-collapse': 'separate',
  20. 'font-family': 'roboto, helvectica, arial, sans-serif',
  21. // Sometimes state strings (`stateHtml`) get long, and because this is an
  22. // absolutely-positioned box, we need to make them wrap around.
  23. 'max-width': '600px',
  24. 'position': 'absolute',
  25. },
  26. heading: {
  27. 'background-color': '#ebf5fb',
  28. 'font-weight': 'bold',
  29. 'text-align': 'center',
  30. ...normalCell
  31. },
  32. normalCell: normalCell,
  33. featureGroup: (componentColor) => ({
  34. 'background-color': componentColor,
  35. 'font-weight': 'bold',
  36. ...normalCell
  37. }),
  38. normalRow: {
  39. 'border': 0,
  40. 'border-collapse': 'separate',
  41. },
  42. };
  43. /**
  44. * Creates table rows that negate IPython/Jupyter notebook styling.
  45. *
  46. * @param {?XML|?Array<XML>} children Child nodes. (Recall Preact handles
  47. * null/undefined gracefully).
  48. * @param {!Object} props Any additional properties.
  49. * @return {!XML} React-y element, representing a table row.
  50. */
  51. const Row = ({children, ...props}) => (
  52. <tr style={style.normalRow} {...props}>{children}</tr>);
  53. /**
  54. * Creates table cells that negate IPython/Jupyter notebook styling.
  55. *
  56. * @param {?XML|?Array<XML>} children Child nodes. (Recall Preact handles
  57. * null/undefined gracefully).
  58. * @param {!Object} props Any additional properties.
  59. * @return {!XML} React-y element, representing a table cell.
  60. */
  61. const Cell = ({children, ...props}) => (
  62. <td style={style.normalCell} {...props}>{children}</td>);
  63. /**
  64. * Construct a table "multi-row" with a shared "header" cell.
  65. *
  66. * In ASCII-art,
  67. *
  68. * ------------------------------
  69. * | row1
  70. * header | row2
  71. * | row3
  72. * ------------------------------
  73. *
  74. * @param {string} headerText Text for the header cell
  75. * @param {string} headerColor Color of the header cell
  76. * @param {!Array<XML>} rowsCells Row cells (<td> React-y elements).
  77. * @return {!Array<XML>} Array of React-y elements.
  78. */
  79. const featureGroup = (headerText, headerColor, rowsCells) => {
  80. const headerCell = (
  81. <td rowspan={rowsCells.length} style={style.featureGroup(headerColor)}>
  82. {headerText}
  83. </td>
  84. );
  85. return _.map(rowsCells, (cells, i) => {
  86. return <Row>{i == 0 ? headerCell : null}{cells}</Row>;
  87. });
  88. };
  89. /**
  90. * Mini helper to intersperse line breaks with a list of elements.
  91. *
  92. * This just replicates previous behavior and looks OK; we could also try spans
  93. * with `display: 'block'` or such.
  94. *
  95. * @param {!Array<XML>} elements React-y elements.
  96. * @return {!Array<XML>} React-y elements with line breaks.
  97. */
  98. const intersperseLineBreaks = (elements) => _.tail(_.flatten(_.map(
  99. elements, (v) => [<br />, v]
  100. )));
  101. export default class NodeInfo extends preact.Component {
  102. /**
  103. * Obligatory Preact render() function.
  104. *
  105. * It might be worthwhile converting some of the intermediate variables into
  106. * stateless functional components, like Cell and Row.
  107. *
  108. * @param {?Object} selected Cytoscape node selected (null if no selection).
  109. * @param {?Object} mousePosition Mouse position, if a node is selected.
  110. * @return {!XML} Preact components to render.
  111. */
  112. render({selected, mousePosition}) {
  113. const visible = selected != null;
  114. const stateHtml = visible && selected.data('stateInfo');
  115. // Generates elements for fixed features.
  116. const fixedFeatures = visible ? selected.data('fixedFeatures') : [];
  117. const fixedFeatureElements = _.map(fixedFeatures, (feature) => {
  118. if (feature.value_trace.length == 0) {
  119. // Preact will just prune this out.
  120. return null;
  121. } else {
  122. const rowsCells = _.map(feature.value_trace, (value) => {
  123. // Recall `value_name` is a list of strings (representing feature
  124. // values), but this is OK because strings are valid react elements.
  125. const valueCells = intersperseLineBreaks(value.value_name);
  126. return [<Cell>{value.feature_name}</Cell>, <Cell>{valueCells}</Cell>];
  127. });
  128. return featureGroup(feature.name, '#cccccc', _.map(rowsCells));
  129. }
  130. });
  131. /**
  132. * Generates linked feature info from an edge.
  133. *
  134. * @param {!Object} edge Cytoscape JS Element representing a linked feature.
  135. * @return {[XML,XML]} Linked feature information, as table elements.
  136. */
  137. const linkedFeatureInfoFromEdge = (edge) => {
  138. return [
  139. <Cell>{edge.data('featureName')}</Cell>,
  140. <Cell>
  141. value {edge.data('featureValue')} from
  142. step {edge.source().data('stepIdx')}
  143. </Cell>
  144. ];
  145. };
  146. const linkedFeatureElements = _.flatten(
  147. _.map(this.edgeStatesByComponent(), (edges, componentName) => {
  148. // Because edges are generated by `incomers`, it is guaranteed to be
  149. // non-empty.
  150. const color = _.head(edges).source().parent().data('componentColor');
  151. const rowsCells = _.map(edges, linkedFeatureInfoFromEdge);
  152. return featureGroup(componentName, color, rowsCells);
  153. }));
  154. let positionOrHiddenStyle;
  155. if (visible) {
  156. positionOrHiddenStyle = {
  157. left: mousePosition.x + 20,
  158. top: mousePosition.y + 10,
  159. };
  160. } else {
  161. positionOrHiddenStyle = {display: 'none'};
  162. }
  163. return (
  164. <table style={_.defaults(positionOrHiddenStyle, style.featuresTable)}>
  165. <Row>
  166. <td colspan="3" style={style.heading}>State</td>
  167. </Row>
  168. <Row>
  169. <Cell colspan="3">{stateHtml}</Cell>
  170. </Row>
  171. <Row>
  172. <td colspan="3" style={style.heading}>Features</td>
  173. </Row>
  174. {fixedFeatureElements}
  175. {linkedFeatureElements}
  176. </table>
  177. );
  178. }
  179. /**
  180. * Gets a list of incoming edges, grouped by their component name.
  181. *
  182. * @return {!Object<string, !Array<!Object>>} Map from component name to list
  183. * of edges.
  184. */
  185. edgeStatesByComponent() {
  186. if (this.props.selected == null) {
  187. return [];
  188. }
  189. const incoming = this.props.selected.incomers(); // edges and nodes
  190. return _.groupBy(incoming.edges(), (edge) => edge.source().parent().id());
  191. }
  192. }