WPF DynamicDataDisplay - медленное построение с маркерами
Мне действительно тяжело ждать, пока ChartPlotter в D3 покажет себя при использовании маркеров. Конечно, я пытаюсь построить записи Gazillion (ну, 700000 записей). При использовании только линии все хорошо (20 секунд или около того). При использовании маркеров мы говорим 5 минут. Это не приемлемо.
Есть идеи?
Вот что я сделал, с объяснениями под ним.
public static string MakeSimplePlot(double[][] xData, double[][] yData, string[] legend, string xAxisTitle, string yAxisTitle, bool[] showLines, bool[] showMarkers)
{
ChartPlotter plotter = new ChartPlotter();
plotter.MainHorizontalAxis = new HorizontalAxis();
plotter.MainVerticalAxis = new VerticalAxis();
HorizontalAxisTitle horizontalAxisTitle = new HorizontalAxisTitle();
horizontalAxisTitle.Content = xAxisTitle;
plotter.AddChild(horizontalAxisTitle);
VerticalAxisTitle verticalAxisTitle = new VerticalAxisTitle();
verticalAxisTitle.Content = yAxisTitle;
plotter.AddChild(verticalAxisTitle);
Color[] plotColors = new Color[13] { Colors.Blue, Colors.Red, Colors.Green, Colors.Chartreuse, Colors.Yellow, Colors.Violet, Colors.Tan, Colors.Silver, Colors.Salmon, Colors.Lime, Colors.Brown, Colors.Chartreuse, Colors.DarkGray };
for (int seriesCounter = 0; seriesCounter < legend.Count(); seriesCounter++)
{
DataFile clearedInputs = ClearExcess(new DataFile(xData[seriesCounter], yData[seriesCounter]));
xData[seriesCounter] = clearedInputs.time;
yData[seriesCounter] = clearedInputs.data;
var xDataSource = new EnumerableDataSource<double>(xData[seriesCounter]);
xDataSource.SetXMapping(x => x);
var yDataSource = new EnumerableDataSource<double>(yData[seriesCounter]);
yDataSource.SetYMapping(x => x);
CompositeDataSource plotSeries = new CompositeDataSource(xDataSource, yDataSource);
CirclePointMarker circlePointMarker = new CirclePointMarker();
circlePointMarker.Fill = new SolidColorBrush(plotColors[seriesCounter]);
circlePointMarker.Pen = new Pen(circlePointMarker.Fill, 0);
circlePointMarker.Size = (showMarkers[seriesCounter] == false) ? 0 : 8;
int lineWidth = (showLines[seriesCounter] == false) ? 0 : 2;
if (showMarkers[seriesCounter] == false)
{
plotter.AddLineGraph(plotSeries, new Pen(circlePointMarker.Fill, lineWidth), new PenDescription("Dummy"));
}
else
{
plotter.AddLineGraph(plotSeries, new Pen(circlePointMarker.Fill, lineWidth), circlePointMarker, new PenDescription("Dummy"));
}
}
UIParameters.plotWindow.mainGrid.Children.Clear();
UIParameters.plotWindow.mainGrid.RowDefinitions.Clear();
UIParameters.plotWindow.mainGrid.Children.Add(plotter);
plotter.Viewport.FitToView();
plotter.LegendVisible = false;
plotter.NewLegendVisible = false;
if (legend.Count() > 1)
{
ShowLegend(legend, plotColors);
}
UIParameters.plotWindow.WindowState = WindowState.Minimized;
UIParameters.plotWindow.WindowState = WindowState.Normal;
string filename = Path.ChangeExtension(Path.GetTempFileName(), "png");
RenderTargetBitmap targetBitmap = new RenderTargetBitmap((int)UIParameters.plotWindow.mainGrid.ActualWidth, (int)UIParameters.plotWindow.mainGrid.ActualHeight, 96d, 96d, PixelFormats.Default);
targetBitmap.Render(UIParameters.plotWindow.mainGrid);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(targetBitmap));
using (var fileStream = File.Open(filename, FileMode.OpenOrCreate))
{
encoder.Save(fileStream);
UIParameters.plotWindow.mainGrid.Clip = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
targetBitmap.Freeze();
if (targetBitmap != null) targetBitmap.Clear();
targetBitmap = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
return filename;
}
Пояснения:
- Я скрываю легенду заговорщика и создаю свою собственную, используя ShowLegend, поскольку легенда не показывает, есть ли у нее только маркеры (я ошибаюсь?)
- Я минимизирую и максимизирую окно графика, поскольку в противном случае график не обновляется или обновляется, но не сохраняется в файле. Это также работает, если я перемещаю окно (я предполагаю, что происходит какое-то событие перерисовки), но, поскольку процесс является автоматическим, пользователь не взаимодействует. Я пытаюсь Invalidate, но безрезультатно. Идеи?
Спасибо!
2 ответа
Я написал свой собственный класс, чтобы скрыть маркеры, когда они за кадром. Это метод виртуализации, который увеличивает производительность в десять раз, когда у вас нет тонны маркеров на экране. Это выглядит так:
using System;
using System.Windows;
using System.Windows.Media;
using Microsoft.Research.DynamicDataDisplay.DataSources;
using Microsoft.Research.DynamicDataDisplay.PointMarkers;
using Microsoft.Research.DynamicDataDisplay.Common;
namespace Microsoft.Research.DynamicDataDisplay.Charts {
public class FilteredMarkerPointsGraph : MarkerPointsGraph {
public FilteredMarkerPointsGraph()
: base() {
;
}
public FilteredMarkerPointsGraph(IPointDataSource dataSource)
: base(dataSource) {
;
}
protected override void OnRenderCore(DrawingContext dc, RenderState state) {
// base.OnRenderCore
if (DataSource == null) return;
if (Marker == null) return;
var left = Viewport.Visible.Location.X;
var right = Viewport.Visible.Location.X + Viewport.Visible.Size.Width;
var top = Viewport.Visible.Location.Y;
var bottom = Viewport.Visible.Location.Y + Viewport.Visible.Size.Height;
var transform = Plotter2D.Viewport.Transform;
DataRect bounds = DataRect.Empty;
using (IPointEnumerator enumerator = DataSource.GetEnumerator(GetContext())) {
Point point = new Point();
while (enumerator.MoveNext()) {
enumerator.GetCurrent(ref point);
if (point.X >= left && point.X <= right && point.Y >= top && point.Y <= bottom)
{
enumerator.ApplyMappings(Marker);
Point screenPoint = point.DataToScreen(transform);
bounds = DataRect.Union(bounds, point);
Marker.Render(dc, screenPoint);
}
}
}
Viewport2D.SetContentBounds(this, bounds);
}
}
Убедитесь, что вы вызываете FilteredMarkerPointsGraph в XAML вместо MarkerPointsGraph!
РЕДАКТИРОВАТЬ
Я не уверен, что вам нужно с легендой с маркерами, я на самом деле не использовал легенду ни в одном из моих графиков, но ваше решение, кажется, хорошо.
Перерисовать сюжет на самом деле довольно просто.
Лучший способ, который я нашел для этого, - это иметь в своем коде свойство, представляющее источник данных, и привязывать источник данных диаграммы к этому свойству. Сделайте так, чтобы ваш код реализовывал INotifyPropertyChanged и вызывал OnPropertyChanged каждый раз, когда вы обновляете или переназначаете свой источник данных. Это заставит плоттер наблюдать за связыванием и перерисовывать ваш график.
Пример:
EnumerableDataSource<Point> m_d3DataSource;
public EnumerableDataSource<Point> D3DataSource {
get {
return m_d3DataSource;
}
set {
//you can set your mapping inside the set block as well
m_d3DataSource = value;
OnPropertyChanged("D3DataSource");
}
}
protected void OnPropertyChanged(PropertyChangedEventArgs e) {
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) {
handler(this, e);
}
}
protected void OnPropertyChanged(string propertyName) {
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
И о вашей производительности с вашими маркерами. Трудно точно определить, что именно вызвало бы вашу производительность, но я рекомендую попробовать использовать другой источник данных. Я использовал EnumerableDataSource, и он всегда работал как шарм. Попробуйте ввести ваши данные в единственном объекте и установить отображение в блоке set, как показано выше, используя:
value.SetYMapping(k => Convert.ToDouble(k.yData));
value.SetXMapping(k => Convert.ToDouble(k.xData));
Единственное, о чем вам нужно беспокоиться - это сопоставление в источнике данных Enumerable, а D3 должен обработать все остальное за вас.
Ну, во всяком случае, пользователь не может видеть маркеры в любом случае, когда вы отображаете "Gazillion" точек: вы не можете переключить режим с линии на маркеры, когда уровень масштабирования более разумный?