-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathHierarchicalDataViewBase.cs
More file actions
398 lines (321 loc) · 12.1 KB
/
Copy pathHierarchicalDataViewBase.cs
File metadata and controls
398 lines (321 loc) · 12.1 KB
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using CSharpCodeAnalyst.TreeMap.Drawing;
using CSharpCodeAnalyst.Contracts;
using CSharpCodeAnalyst.TreeMap.Interfaces;
using CSharpCodeAnalyst.TreeMap.Tools;
using Application = System.Windows.Application;
using ContextMenu = System.Windows.Controls.ContextMenu;
using MenuItem = System.Windows.Controls.MenuItem;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
using UserControl = System.Windows.Controls.UserControl;
namespace CSharpCodeAnalyst.TreeMap;
/// <summary>
/// Note about the coloring:
/// Filtering is always done on a clone of the original tree. Filtering leads to recalculation of the weights (colors).
/// This happens when we change the filters in the tool window.
/// Changing the zoom level only sets a different entry point to display. It does not recalculate the weights.
/// Therefore, the zoom level does not affect the coloring even if the data with the most significant weights are not
/// visible.
/// </summary>
public abstract class HierarchicalDataViewBase : UserControl
{
public static readonly DependencyProperty UserCommandsProperty = DependencyProperty.Register(
nameof(UserCommands), typeof(HierarchicalDataCommands), typeof(HierarchicalDataViewBase),
new PropertyMetadata(null));
private readonly MenuItem _toolMenuItem = new()
{ Header = "Tools", Tag = null };
/// <summary>
/// Original data, untouched
/// </summary>
private IHierarchicalData? _originalData;
private IRenderer? _renderer;
private ToolView? _toolView;
private ToolViewModel? _toolViewModel;
/// <summary>
/// Sub tree to display. This is the current zoom level shown.
/// The weights (colors) are not adjusted when we change the zoom level.
/// This may be a clone of the original data (when filtered) or
/// a reference to the original data (no filter)
/// </summary>
private IHierarchicalData? _zoomLevel;
/// <summary>
/// The weight-to-color mapping currently applied. Chosen in the tool window; kept on the view
/// (which is reused across tabs) so the choice persists while the app runs.
/// </summary>
private WeightNormalizationStrategy _weightNormalization = WeightNormalizationStrategy.RankPercentile;
/// <summary>
/// Supplied by the data context: creates the placeholder shown when a filter removes every
/// node. The control only knows the interface, so it cannot build a concrete node itself.
/// </summary>
private Func<IHierarchicalData>? _createNoData;
protected IBrushFactory? BrushFactory { get; set; }
/// <summary>
/// Commands that apply to leaf nodes of a hierarchical data.
/// </summary>
public HierarchicalDataCommands? UserCommands
{
get => (HierarchicalDataCommands)GetValue(UserCommandsProperty);
set => SetValue(UserCommandsProperty, value);
}
protected void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// The single TreeMapView instance is reused across all tree-map tabs (same DataTemplate),
// so switching tabs just rebinds it. The tool window belongs to the data we were showing -
// close it here so the newly shown tab can open its own.
HideToolView();
_originalData = null;
BrushFactory = null;
if (DataContext is not HierarchicalDataContext context)
{
// This is called once with the wrong context.
return;
}
if (context.AreaSemantic == null || context.WeightSemantic == null)
{
return;
}
BrushFactory = context.BrushFactory;
_originalData = context.Data;
_createNoData = context.CreateNoData;
InitializeTools(context.AreaSemantic, context.WeightSemantic);
// Weights arrive raw - the view owns the normalization. DoFilter re-normalizes
// whenever leaves are removed by a filter change.
_originalData.NormalizeWeightMetrics(_weightNormalization);
// Initially no filtering so skip removing nodes.
ZoomLevelChanged(_originalData);
}
private void OnToolHighlightPatternChanged(object? sender, EventArgs args)
{
if (_zoomLevel is null)
{
return;
}
// Render again with new highlighting
DoRender(_zoomLevel);
}
private void ChangeZoomLevelCommand(IHierarchicalData? item)
{
if (item == null)
{
return;
}
// Note: source tree is already filtered.
ZoomLevelChanged(item);
}
protected abstract void ClosePopup();
protected abstract IRenderer CreateRenderer();
private IHierarchicalData DoFilter(IHierarchicalData data)
{
if (_toolViewModel is null || _toolViewModel.NoFilterJustHighlight)
{
// Highlighting the filter instead of removing the nodes.
return data;
}
data.RemoveLeafNodes(leaf =>
!_toolViewModel.IsAreaValid(leaf.AreaMetric) ||
!_toolViewModel.IsWeightValid(leaf.WeightMetric));
try
{
data.RemoveLeafNodesWithoutArea();
}
catch (Exception)
{
// The filter removed everything - show the producer-supplied placeholder.
if (_createNoData != null)
{
data = _createNoData();
}
}
// After we removed weights we have to normalize again.
data.SumAreaMetrics(); // Only TreeMapView
data.NormalizeWeightMetrics(_weightNormalization);
return data;
}
private void OnToolFilterChanged(object? sender, EventArgs args)
{
if (_originalData is null)
{
return;
}
var oldZoomLevelPath = _zoomLevel?.GetPathToRoot();
var sourceData = DoFilter(_originalData.Clone());
var zoomTo = FindByPath(sourceData, oldZoomLevelPath);
ZoomLevelChanged(zoomTo);
}
private IHierarchicalData FindByPath(IHierarchicalData tree, string? path)
{
if (path == null)
{
return tree;
}
// Re-locate the previously zoomed node in the freshly filtered clone by its path to
// root. A node's path is unique (siblings never share a name) and stable across the
// clone, so it identifies the same logical node without a separate Id field. Falls back
// to the root when the node no longer exists (its subtree was filtered away).
return tree.FirstOrDefault(node => node.GetPathToRoot() == path) ?? tree;
}
protected abstract DrawingCanvas GetCanvas();
protected void HideToolView()
{
// When the control is no longer visible close the tool window.
_toolView?.Close();
_toolView = null;
_zoomLevel = null;
}
private void InitializeTools(string areaSemantic, string weightSemantic)
{
var area = new HashSet<double>();
var weight = new HashSet<double>();
if (_originalData is null)
{
return;
}
// Distinct areas and weights. Each slider tick goes to the next value.
// This allows smooth navigation even if there are large outliers.
_originalData.TraverseTopDown(data =>
{
if (data.IsLeafNode)
{
area.Add(data.AreaMetric);
weight.Add(data.WeightMetric);
}
});
var areaList = area.OrderBy(x => x).ToList();
var weightList = weight.OrderBy(x => x).ToList();
_toolViewModel = new ToolViewModel(areaList, weightList)
{
AreaSemantic = areaSemantic,
WeightSemantic = weightSemantic,
WeightNormalization = _weightNormalization
};
_toolViewModel.FilterChanged += OnToolFilterChanged;
_toolViewModel.HighlightPatternChanged += OnToolHighlightPatternChanged;
_toolViewModel.Reset += OnToolReset;
_toolViewModel.WeightNormalizationChanged += OnToolWeightNormalizationChanged;
}
private void OnToolWeightNormalizationChanged(object? sender, EventArgs e)
{
if (_toolViewModel is null || _originalData is null)
{
return;
}
_weightNormalization = _toolViewModel.WeightNormalization;
// Re-map the untouched original (so a later Reset shows the new coloring too), then rebuild
// the currently displayed - possibly filtered - view with the same zoom preserved.
_originalData.NormalizeWeightMetrics(_weightNormalization);
OnToolFilterChanged(sender, e);
}
private void OnToolReset(object? sender, EventArgs e)
{
if (_originalData == null)
{
return;
}
ZoomLevelChanged(_originalData);
}
protected abstract void InitPopup(IHierarchicalData hit);
protected ContextMenu? GetContextMenu(object sender)
{
var fe = sender as FrameworkElement;
return fe?.ContextMenu;
}
protected void OnContextMenuOpening(object sender, ContextMenuEventArgs e)
{
// Does not tell which one.
if (_renderer == null || _zoomLevel == null)
{
e.Handled = true;
return;
}
var canvas = GetCanvas();
var pos = _renderer.Transform(Mouse.GetPosition(canvas));
var hit = _renderer.Hit(_zoomLevel, pos);
var menu = GetContextMenu(sender);
if (hit != null && menu != null)
{
menu.Items.Clear();
// Item for filter tool window
_toolMenuItem.IsEnabled = _toolView is not { IsVisible: true };
_toolMenuItem.Command = new DelegateCommand(ShowToolsCommand);
menu.Items.Add(_toolMenuItem);
UserCommands?.Fill(menu, hit);
menu.Items.Add(new Separator());
FillZoomLevels(menu, hit);
}
// Show context menu if at least one item is there.
e.Handled = menu?.Items.Count == 0;
}
private void ShowToolsCommand()
{
// Filter
_toolView = new ToolView
{
Owner = Application.Current.MainWindow,
DataContext = _toolViewModel
};
_toolView.Show();
}
protected void Window_MouseLeave(object sender, MouseEventArgs e)
{
ClosePopup();
}
protected void Window_MouseMove(object sender, MouseEventArgs e)
{
if (_originalData == null || _renderer == null || _zoomLevel == null)
{
return;
}
ClosePopup();
// Circle packing renderer uses transformations. So we have to translate the mouse position
// into the coordinates of the circles.
var pos = _renderer.Transform(e.GetPosition(GetCanvas()));
var hit = _renderer.Hit(_zoomLevel, pos);
if (hit != null)
{
InitPopup(hit);
}
}
private void ZoomLevelChanged(IHierarchicalData? data)
{
if (data == null)
{
return;
}
DoRender(data);
}
private void DoRender(IHierarchicalData data)
{
_zoomLevel = data;
_renderer = CreateRenderer();
_renderer.LoadData(_zoomLevel);
_renderer.Highlighting = new Highlighting(_toolViewModel!);
GetCanvas().DataContext = _renderer;
}
private void AddZoomLevel(ContextMenu menu, IHierarchicalData data)
{
var header = data.GetPathToRoot();
var menuItem = new MenuItem
{
Header = header,
Command = new DelegateCommand(() => ChangeZoomLevelCommand(data))
};
menu.Items.Add(menuItem);
}
private void FillZoomLevels(ContextMenu menu, IHierarchicalData hit)
{
// From the current item (exclusive) up the the root
// add an context menu entry for each zoom level.
var current = hit;
while (current != null)
{
// Avoid unnecessary context menus
if (current != _zoomLevel && !current.IsLeafNode)
{
AddZoomLevel(menu, current);
}
current = current.Parent;
}
}
}