node_info.jsx 6.31 KB
Newer Older
Ivan Bogatyy's avatar
Ivan Bogatyy committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211

/**
 * Template for node info.
 */
import preact from 'preact';
import _ from 'lodash';

const normalCell = {
  'border': 0,
  'border-collapse': 'separate',
  'padding': '2px',
};

/**
 * Style definitions which are directly injected (see README.md comments).
 */
const style = {
  featuresTable: {
    'background-color': 'rgba(255, 255, 255, 0.9)',
    'border': '1px solid #dddddd',
    'border-spacing': '2px',
    'border-collapse': 'separate',
    'font-family': 'roboto, helvectica, arial, sans-serif',
    // Sometimes state strings (`stateHtml`) get long, and because this is an
    // absolutely-positioned box, we need to make them wrap around.
    'max-width': '600px',
    'position': 'absolute',
  },

  heading: {
    'background-color': '#ebf5fb',
    'font-weight': 'bold',
    'text-align': 'center',
    ...normalCell
  },

  normalCell: normalCell,

  featureGroup: (componentColor) => ({
    'background-color': componentColor,
    'font-weight': 'bold',
    ...normalCell
  }),

  normalRow: {
    'border': 0,
    'border-collapse': 'separate',
  },
};

/**
 * Creates table rows that negate IPython/Jupyter notebook styling.
 *
 * @param {?XML|?Array<XML>} children Child nodes. (Recall Preact handles
 *     null/undefined gracefully).
 * @param {!Object} props Any additional properties.
 * @return {!XML} React-y element, representing a table row.
 */
const Row = ({children, ...props}) => (
  <tr style={style.normalRow} {...props}>{children}</tr>);

/**
 * Creates table cells that negate IPython/Jupyter notebook styling.
 *
 * @param {?XML|?Array<XML>} children Child nodes. (Recall Preact handles
 *     null/undefined gracefully).
 * @param {!Object} props Any additional properties.
 * @return {!XML} React-y element, representing a table cell.
 */
const Cell = ({children, ...props}) => (
  <td style={style.normalCell} {...props}>{children}</td>);

/**
 * Construct a table "multi-row" with a shared "header" cell.
 *
 * In ASCII-art,
 *
 * ------------------------------
 *        | row1
 * header | row2
 *        | row3
 * ------------------------------
 *
 * @param {string} headerText Text for the header cell
 * @param {string} headerColor Color of the header cell
 * @param {!Array<XML>} rowsCells Row cells (<td> React-y elements).
 * @return {!Array<XML>} Array of React-y elements.
 */
const featureGroup = (headerText, headerColor, rowsCells) => {
  const headerCell = (
    <td rowspan={rowsCells.length} style={style.featureGroup(headerColor)}>
      {headerText}
    </td>
  );
  return _.map(rowsCells, (cells, i) => {
    return <Row>{i == 0 ? headerCell : null}{cells}</Row>;
  });
};

/**
 * Mini helper to intersperse line breaks with a list of elements.
 *
 * This just replicates previous behavior and looks OK; we could also try spans
 * with `display: 'block'` or such.
 *
 * @param {!Array<XML>} elements React-y elements.
 * @return {!Array<XML>} React-y elements with line breaks.
 */
const intersperseLineBreaks = (elements) => _.tail(_.flatten(_.map(
  elements, (v) => [<br />, v]
)));

export default class NodeInfo extends preact.Component {
  /**
   * Obligatory Preact render() function.
   *
   * It might be worthwhile converting some of the intermediate variables into
   * stateless functional components, like Cell and Row.
   *
   * @param {?Object} selected Cytoscape node selected (null if no selection).
   * @param {?Object} mousePosition Mouse position, if a node is selected.
   * @return {!XML} Preact components to render.
   */
  render({selected, mousePosition}) {
    const visible = selected != null;
    const stateHtml = visible && selected.data('stateInfo');

    // Generates elements for fixed features.
    const fixedFeatures = visible ? selected.data('fixedFeatures') : [];
    const fixedFeatureElements = _.map(fixedFeatures, (feature) => {
      if (feature.value_trace.length == 0) {
        // Preact will just prune this out.
        return null;
      } else {
        const rowsCells = _.map(feature.value_trace, (value) => {
          // Recall `value_name` is a list of strings (representing feature
          // values), but this is OK because strings are valid react elements.
          const valueCells = intersperseLineBreaks(value.value_name);
          return [<Cell>{value.feature_name}</Cell>, <Cell>{valueCells}</Cell>];
        });
        return featureGroup(feature.name, '#cccccc', _.map(rowsCells));
      }
    });

    /**
     * Generates linked feature info from an edge.
     *
     * @param {!Object} edge Cytoscape JS Element representing a linked feature.
     * @return {[XML,XML]} Linked feature information, as table elements.
     */
    const linkedFeatureInfoFromEdge = (edge) => {
      return [
        <Cell>{edge.data('featureName')}</Cell>,
        <Cell>
          value {edge.data('featureValue')} from
          step {edge.source().data('stepIdx')}
        </Cell>
      ];
    };

    const linkedFeatureElements = _.flatten(
      _.map(this.edgeStatesByComponent(), (edges, componentName) => {
        // Because edges are generated by `incomers`, it is guaranteed to be
        // non-empty.
        const color = _.head(edges).source().parent().data('componentColor');
        const rowsCells = _.map(edges, linkedFeatureInfoFromEdge);
        return featureGroup(componentName, color, rowsCells);
      }));

    let positionOrHiddenStyle;
    if (visible) {
      positionOrHiddenStyle = {
        left: mousePosition.x + 20,
        top: mousePosition.y + 10,
      };
    } else {
      positionOrHiddenStyle = {display: 'none'};
    }

    return (
      <table style={_.defaults(positionOrHiddenStyle, style.featuresTable)}>
        <Row>
          <td colspan="3" style={style.heading}>State</td>
        </Row>
        <Row>
          <Cell colspan="3">{stateHtml}</Cell>
        </Row>
        <Row>
          <td colspan="3" style={style.heading}>Features</td>
        </Row>
        {fixedFeatureElements}
        {linkedFeatureElements}
      </table>
    );
  }

  /**
   * Gets a list of incoming edges, grouped by their component name.
   *
   * @return {!Object<string, !Array<!Object>>} Map from component name to list
   *     of edges.
   */
  edgeStatesByComponent() {
    if (this.props.selected == null) {
      return [];
    }
    const incoming = this.props.selected.incomers();  // edges and nodes
    return _.groupBy(incoming.edges(), (edge) => edge.source().parent().id());
  }
}