3 votes

WPF DynamicDataDisplay - Tracé lent avec des marqueurs

Je trouve très difficile d'attendre que le ChartPlotter dans D3 se montre, quand j'utilise des marqueurs. Bien sûr, j'essaie de tracer un gazillion d'enregistrements (enfin, 700 000 enregistrements). Quand j'utilise juste une ligne, tout va bien (environ 20 secondes). Quand j'utilise des marqueurs, cela prend 5 minutes. Ce n'est pas acceptable.

Des idées ?

Voici ce que j'ai fait, avec des explications en dessous :

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(xData[seriesCounter]);
            xDataSource.SetXMapping(x => x);

            var yDataSource = new EnumerableDataSource(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;
    }

Explications :

  1. Je cache la légende du plotter et crée la mienne en utilisant ShowLegend, car la légende ne s'affiche pas si elle n'a que des marqueurs (me trompé-je ?)
  2. Je réduis et maximise la fenêtre du tracé, car sinon le tracé ne se met pas à jour, ou il se met à jour mais ne se sauvegarde pas dans un fichier. Cela fonctionne aussi si je déplace la fenêtre (je suppose qu'il s'agit d'un genre d'événement de redessin), mais comme le processus est automatique, l'utilisateur n'a pas d'interaction. J'ai essayé Invalidate, sans succès. Des idées ?

Merci !

3voto

Jason Higgins Points 1028

J'ai écrit ma propre classe pour masquer les marqueurs lorsqu'ils ne sont pas à l'écran. C'est une technique de virtualisation qui accélère les performances de dix fois lorsque vous n'avez pas des tonnes de marqueurs à l'écran. Cela ressemble à ceci :

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);
    }
}

Assurez-vous d'appeler FilteredMarkerPointsGraph dans le XAML au lieu de MarkerPointsGraph!

MODIFICATION

  1. Je ne suis pas sûr de ce dont vous avez besoin avec la légende des marqueurs, je n'ai en fait pas utilisé de légende dans l'un de mes graphiques, mais votre solution semble être bonne.

  2. Redessiner le graphique est en fait assez facile.

La meilleure façon que j'ai trouvée de le faire est d'avoir une propriété dans votre code-behind qui représente la DataSource et de lier la DataSource du graphique à cette propriété. Faites en sorte que votre code-behind implémente INotifyPropertyChanged et appelez OnPropertyChanged à chaque fois que vous mettez à jour ou réassignez votre source de données. Cela forcera le traceur à observer la liaison et à redessiner votre graphique.

Exemple :

EnumerableDataSource m_d3DataSource;
public EnumerableDataSource D3DataSource {
get {
    return m_d3DataSource;
}
set {                
    //vous pouvez également définir votre mappage à l'intérieur du bloc set              
    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));
}  

Et en ce qui concerne les performances de vos marqueurs.. Il est difficile de déterminer exactement ce qui pourrait causer votre problème de performances, mais ma recommandation est d'essayer d'utiliser une source de données différente. J'ai utilisé EnumerableDataSource et cela a toujours fonctionné à merveille. Essayez d'amener vos données dans un objet unique et de définir le mappage dans votre bloc set comme indiqué ci-dessus en utilisant :

value.SetYMapping(k => Convert.ToDouble(k.yData));
value.SetXMapping(k => Convert.ToDouble(k.xData));

La seule chose à laquelle vous devez faire attention est le mappage dans EnumerableDataSource et D3 devrait gérer le reste pour vous.

1voto

Felice Pollano Points 20105

Eh bien, l'utilisateur ne peut probablement pas voir les marqueurs de toute façon lorsque vous affichez les "millions" de points : ne pouvez-vous pas passer du mode ligne aux marqueurs lorsque le niveau de zoom est plus raisonnable ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X