File size: 6,022 Bytes
ef4c917
f277792
1e3df84
 
 
 
 
 
ef4c917
f277792
 
046e1e3
1e3df84
 
f277792
046e1e3
f277792
 
 
 
 
 
 
 
046e1e3
f277792
ef4c917
f277792
1e3df84
046e1e3
 
 
 
 
 
 
 
1e3df84
 
 
ef4c917
 
 
 
1e3df84
ef4c917
 
 
 
 
 
1e3df84
ef4c917
 
 
 
 
 
1e3df84
ef4c917
1e3df84
ef4c917
 
 
 
1e3df84
ef4c917
 
 
 
1e3df84
f277792
 
 
 
 
 
 
 
 
046e1e3
 
f277792
 
 
 
046e1e3
f277792
 
 
 
 
 
 
046e1e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0e4431
 
046e1e3
f277792
046e1e3
 
f277792
 
 
 
046e1e3
 
 
 
f277792
 
046e1e3
f277792
046e1e3
f277792
 
 
 
 
 
 
ef4c917
 
 
 
 
 
1e3df84
ef4c917
1e3df84
ef4c917
 
 
 
 
 
 
 
 
 
 
 
1e3df84
ef4c917
1e3df84
 
 
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
import { useState } from 'react';
import type { GraphStatistics, ConnectivityInfo } from '../types';
import { NODE_TYPE_LABELS, NODE_COLORS } from '../constants';

interface Props {
  statistics: GraphStatistics;
  processingTime: number;
  imageName: string;
  edited?: boolean;
  connectivity?: ConnectivityInfo | null;
  onFocusNode?: (id: string) => void;
  onToggleBroken?: (show: boolean) => void;
}

const MAX_LISTED = 40;
const REASON_LABEL: Record<string, string> = { isolated: 'no edges', room: 'room', exit: 'exit' };

export default function StatsPanel({
  statistics,
  processingTime,
  imageName,
  edited,
  connectivity,
  onFocusNode,
  onToggleBroken,
}: Props) {
  const [open, setOpen] = useState(true);
  const [connOpen, setConnOpen] = useState(false);
  const { total_nodes, total_edges, node_types, pruning_reduction } = statistics;
  const broken = connectivity && !connectivity.fullyConnected;

  const toggleConn = () =>
    setConnOpen((o) => {
      const next = !o;
      onToggleBroken?.(next);
      return next;
    });

  return (
    <div className="panel">
      <button className="panel-collapse-header" onClick={() => setOpen((o) => !o)}>
        <h3>Statistics{edited ? ' (edited)' : ''}</h3>
        <span className={`chevron${open ? ' open' : ''}`}></span>
      </button>

      {open && (
        <div className="panel-collapse-body">
          <div className="stat-row">
            <span className="label">Image</span>
            <span className="value stat-image" title={imageName}>{imageName}</span>
          </div>

          <div className="stat-row">
            <span className="label">Processing Time</span>
            <span className="value">
              {processingTime > 0 ? `${processingTime.toFixed(1)}s` : 'Cached'}
            </span>
          </div>

          <hr className="stat-divider" />

          <div className="stat-row">
            <span className="label">Total Nodes</span>
            <span className="value">{total_nodes}</span>
          </div>

          <div className="stat-row">
            <span className="label">Total Edges</span>
            <span className="value">{total_edges}</span>
          </div>

          {connectivity && (
            <>
              <div className="stat-row">
                <span className="label">Connectivity</span>
                <span className="value">
                  {connectivity.score}%{' '}
                  {broken ? (
                    <button
                      className="conn-warn"
                      title="Issues found. Click to inspect and highlight on the graph."
                      onClick={toggleConn}
                    >
                      &#9888;
                    </button>
                  ) : (
                    <span className="conn-ok" title="Rooms and exits fully connected">&#10003;</span>
                  )}
                </span>
              </div>

              {broken && connOpen && (
                <div className="conn-list">
                  <div className="conn-list-title">
                    {connectivity.isolatedCount > 0 && (
                      <div className="conn-flag">
                        {connectivity.isolatedCount} node
                        {connectivity.isolatedCount === 1 ? '' : 's'} with no edges
                      </div>
                    )}
                    {connectivity.roomsDisconnected > 0 && (
                      <div>
                        {connectivity.roomsDisconnected} room
                        {connectivity.roomsDisconnected === 1 ? '' : 's'} unreachable
                      </div>
                    )}
                    {connectivity.exitsDisconnected > 0 && (
                      <div>
                        {connectivity.exitsDisconnected} exit door
                        {connectivity.exitsDisconnected === 1 ? '' : 's'} unreachable
                      </div>
                    )}
                    <div className="conn-locate">
                      The smaller, disconnected subgraph is highlighted in red on
                      the graph. Click a node to zoom to it.
                    </div>
                  </div>
                  {connectivity.offenders.slice(0, MAX_LISTED).map((n) => (
                    <button key={n.id} className="conn-node" onClick={() => onFocusNode?.(n.id)}>
                      <span
                        className="type-swatch"
                        style={{ backgroundColor: NODE_COLORS[n.type] || '#999' }}
                      />
                      <span className="conn-node-id">{n.id}</span>
                      <span className={`conn-reason conn-reason-${n.reason}`}>
                        {REASON_LABEL[n.reason]}
                      </span>
                    </button>
                  ))}
                  {connectivity.offenders.length > MAX_LISTED && (
                    <div className="conn-more">
                      + {connectivity.offenders.length - MAX_LISTED} more
                    </div>
                  )}
                </div>
              )}
            </>
          )}

          {pruning_reduction != null && pruning_reduction > 0 && (
            <div className="stat-row">
              <span className="label">Pruning Reduction</span>
              <span className="value">{pruning_reduction}%</span>
            </div>
          )}

          <hr className="stat-divider" />

          {Object.entries(node_types).map(([type, count]) => (
            <div className="stat-row" key={type}>
              <span className="label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                <span
                  className="type-swatch"
                  style={{ backgroundColor: NODE_COLORS[type] || '#999' }}
                />
                {NODE_TYPE_LABELS[type] || type}
              </span>
              <span className="value">{count}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}