给DynamicDataDisplay添加BrokenLineGraph

   之前在做项目的时候遇到一个需求,显示一条曲线,按时间区分产品的当前状态,不同的状态要求使用不同的颜色来区分,因为之前曾经用过D3来做过项目,所以自然的想到继续使用D3来实现。

   1. 实现思路:

     由于产品只有3种状态,所以使用3条曲线来组合显示一条曲线,每条曲线定义不同的颜色即可,这个办法最简单,也最容易实现。

     只使用一条曲线,曲线按时间段来设定颜色,这样只需要一个数据源就可以了,上层调用的时候逻辑更简单。

    实际情况:2种思路都无法使用D3来实现……,D3既不支持BrokenLineGraph,也不支持MultColorLine,Google了几天也没有找到办法,倒是找到一个替代的控件,Oxyplot,

    查阅文档以及建立Test Sample后发现,Oxyplot虽然支持BrokenLineGraph,功能也比D3更多,但是不支持在数据源更新后自动更新显示!必须手动调用控件的刷新方法,或者在自己的ViewModel内内嵌一个PoltViewModel,调用PoltViewModel的刷新方法,才可以更新界面显示;而且测试发现似乎Oxyplot的性能不及D3的好,而且我也不想在自己的ViewModel里内嵌一个PoltViewModel,那只有掉头回去使用D3了。


  2. 自己造轮子

     前面提到过Google了几天也没有找到让D3实现BrokenLineGraph或者MultColorLine,那就只有自己造轮子了,项目已经做到一半了,看着DeadLine选择了前面的第一种思路,自己实现BrokenLineGraph:在曲线断点的地方添加dobule.NaN,恢复点使用实际数据就可以了。

    实验发现,只要往数据源里添加Point(dobule.Nan, dobule.Nan),已经显示在D3上的曲线就会消失,在查阅源代码及单步执行后发现,当进入BoundsHelper.GetDataBounds时,已经显示的曲线就会消失,查阅源代码可以发现,更新数据源后,D3会根据添加的数据点计算出当前曲线的边界,并根据返回的边界来决定那些数据点将会显示在界面上,从下面的源代码可以看出,当数据点Point(dobule.NaN, dobule.NaN)添加到数据源后,BoundsHelper.GetDataBounds返回的边界是无意义的,所以才会造成曲线消失:

  

		/// <summary>Computes bounding rectangle for sequence of points</summary>
		/// <param name="points">Points sequence</param>
		/// <returns>Minimal axis-aligned bounding rectangle</returns>
		public static Rect GetDataBounds(IEnumerable<Point> points)
		{
			Rect bounds = Rect.Empty;

			double xMin = Double.PositiveInfinity;
			double xMax = Double.NegativeInfinity;

			double yMin = Double.PositiveInfinity;
			double yMax = Double.NegativeInfinity;

			foreach (Point p in points)
			{
				xMin = Math.Min(xMin, p.X);
				xMax = Math.Max(xMax, p.X);

				yMin = Math.Min(yMin, p.Y);
				yMax = Math.Max(yMax, p.Y);
			}

			// were some points in collection
			if (!Double.IsInfinity(xMin))
			{
				bounds = MathHelper.CreateRectByPoints(xMin, yMin, xMax, yMax);
			}

			return bounds;
		}

从上面的代码可以知道: 当数据点Point(dobule.NaN, dobule.NaN)添加到数据源后,返回Bound的长宽都为dobule.NaN,所以我们需要将断点数据丢掉:

在循环内添加代码:

                if (double.IsNaN(p.X) || double.IsNaN(p.Y))
                    continue;

这样返回的Bound就不会丢失了。

    好了,BoundsHelper修改好了,现在该实现BrokenLineGraph了,先看看D3的LineGraph是如何实现的:

    重写PointsGraphBase的UpdateCore方法,将数据源内的数据点转换为实际显示在屏幕上的坐标数据点;重写PointsGraphBase的OnRenderCore方法,将UpdateCore更新的FakePointList转换为StreamGeometry并调用DrawingContext的DrawGeometry方法,将数据点显示到界面上。

    明白实现原理就好办了,由于StreamGeometry不接受无意义数据点Point(dobule.NaN, dobule.NaN),所以不能直接将断点数据写到StreamGeometry内,那就在UpdateCore内生成多个FakePointList就好了,有多少个FakePointList,就写多少个StreamGeometry到DrawingContext上去,这样虽然不够优雅,但是在当前项目的实际情况下,可以快速解决问题,就这么办了,下面是UpdateCore的代码:

        #region UpdateCore
        protected override void UpdateCore()
        {
            if (DataSource == null)
            {
                return;
            }
            
            Rect output = Viewport.Output;
            var transform = GetTransform();

            if (FakePointLists == null || !(transform.DataTransform is IdentityTransform))
            {
                IEnumerable<Point> AllPoints = GetPoints();

                FakePointLists = new List<FakePointList>();

                transform = GetTransform();

                Point CurrentPoint;
                List<Point> transformedPoints;
                IEnumerator<Point> enumerator = AllPoints.GetEnumerator();
                ObservableDataSource<Point> list = new ObservableDataSource<Point>();
                
                bool HasNext = enumerator.MoveNext();

                bool CurrentIsBroken = false;

                while (HasNext)
                {
                    CurrentPoint = enumerator.Current;

                    HasNext = enumerator.MoveNext();

                    CurrentIsBroken = double.IsNaN(CurrentPoint.X) || double.IsNaN(CurrentPoint.Y);



                    if (CurrentIsBroken || !HasNext)
                    {
                        if (list.Collection.Count > 0)
                        {
                            if (!CurrentIsBroken && !HasNext)
                            {
                                list.Collection.Add(CurrentPoint);
                            }
                            transformedPoints = transform.DataToScreen(list.GetPoints());

                            FakePointLists.Add(new FakePointList(FilterPoints(transformedPoints),
                                            output.Left, output.Right));

                            Offset = new Vector();

                            list = new ObservableDataSource<Point>();
                        }
                        CurrentIsBroken = false;
                    }
                    else
                    {
                        list.Collection.Add(CurrentPoint);
                    }
                }
            }
            else
            {
                double left = output.Left;
                double right = output.Right;
                double shift = Offset.X;
                left -= shift;
                right -= shift;

                foreach (var filteredPoints in FakePointLists)
                {
                    filteredPoints.SetXBorders(left, right);
                }
            }
        }
        #endregion
  建立一个FakePointList的List,然后在OnRenderCore内转换为StreamGeometry写入到DrawingContext:

         

        #region OnRenderCore
        protected override void OnRenderCore(DrawingContext dc, RenderState state)
        {
            if (DataSource == null) return;

            if (FakePointLists != null)
            {
                StreamGeometry geometry = new StreamGeometry();
                using (StreamGeometryContext context = geometry.Open())
                {
                    foreach (var filteredPoints in FakePointLists)
                    {
                        if(filteredPoints.HasPoints)
                        {
                            context.BeginFigure(filteredPoints.StartPoint, false, false);
                            context.PolyLineTo(filteredPoints, true, true);
                        }
                    }
                }
                geometry.Freeze();

                const Brush brush = null;

                Pen pen = LinePen;
                
                bool isTranslated = IsTranslated;

                if (isTranslated)
                {
                    dc.PushTransform(new TranslateTransform(Offset.X, Offset.Y));
                }

                dc.DrawGeometry(brush, pen, geometry);

                if (isTranslated)
                {
                    dc.Pop();
                }
            }
        }
        #endregion 

 下面是实测效果:



  下面是完整的BrokenLineGraph.cs:

    /// <summary>
    /// 需要修改PenDescription类的AttachCore方法
    /// BoundsHelper.GetDataBounds
    /// </summary>
    public class BrokenLineGraph : PointsGraphBase
    {
        #region Constructor

        #region BrokenLineGraph
        /// <summary>
        /// Initializes a new instance of the <see cref="LineGraph"/> class.
        /// </summary>
        public BrokenLineGraph()
        {
            Legend.SetVisibleInLegend(this, true);
            ManualTranslate = true;

            filters.CollectionChanged += filters_CollectionChanged;
        }
        #endregion

        #region public LineGraph(IPointDataSource pointSource: this()
        /// <summary>
        /// Initializes a new instance of the <see cref="LineGraph"/> class.
        /// </summary>
        /// <param name="pointSource">The point source.</param>
        public BrokenLineGraph(IPointDataSource pointSource)
            : this()
        {
            DataSource = pointSource;
        }
        #endregion 

        #endregion

        #region Filters

        #region filters
        /// <summary>Filters applied to points before rendering</summary>
        private readonly FilterCollection filters = new FilterCollection();
        #endregion

        #region Filters
        /// <summary>Provides access to filters collection</summary>
        public FilterCollection Filters
        {
            get { return filters; }
        }
        #endregion

        #region FilterPoints
        private List<Point> FilterPoints(List<Point> points)
        {
            if (!filteringEnabled)
                return points;

            var filteredPoints = filters.Filter(points, Viewport.Output);

            return filteredPoints;
        }
        #endregion

        #region FilteringEnabled
        private bool filteringEnabled = true;
        public bool FilteringEnabled
        {
            get { return filteringEnabled; }
            set
            {
                if (filteringEnabled != value)
                {
                    filteringEnabled = value;
                    FakePointLists = null;
                    Update();
                }
            }
        }
        #endregion

        #region filters_CollectionChanged
        private void filters_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            FakePointLists = null;
            Update();
        }
        #endregion 

        #endregion

        #region FakePointLists

        private List<FakePointList> FakePointLists;

        #endregion

        #region override

        #region OnDataSourceChanged
        protected override void OnDataSourceChanged(DependencyPropertyChangedEventArgs args)
        {
            if (args.NewValue != args.OldValue)
            {
                FakePointLists = null;
            }
            
            base.OnDataSourceChanged(args);
        } 
        #endregion

        #region OnOutputChanged
        protected override void OnOutputChanged(Rect newRect, Rect oldRect)
        {
            FakePointLists = null;

            base.OnOutputChanged(newRect, oldRect);
        }
        #endregion

        #region OnDataChanged
        protected override void OnDataChanged()
        {
            FakePointLists = null;

            base.OnDataChanged();
        }
        #endregion

        #region OnVisibleChanged
        protected override void OnVisibleChanged(Rect newRect, Rect oldRect)
        {
            if (newRect.Size != oldRect.Size)
            {
                FakePointLists = null;
            }

            base.OnVisibleChanged(newRect, oldRect);
        }
        #endregion

        #region UpdateCore
        protected override void UpdateCore()
        {
            if (DataSource == null)
            {
                return;
            }
            
            Rect output = Viewport.Output;
            var transform = GetTransform();

            if (FakePointLists == null || !(transform.DataTransform is IdentityTransform))
            {
                IEnumerable<Point> AllPoints = GetPoints();

                FakePointLists = new List<FakePointList>();

                transform = GetTransform();

                Point CurrentPoint;
                List<Point> transformedPoints;
                IEnumerator<Point> enumerator = AllPoints.GetEnumerator();
                ObservableDataSource<Point> list = new ObservableDataSource<Point>();
                
                bool HasNext = enumerator.MoveNext();

                bool CurrentIsBroken = false;

                while (HasNext)
                {
                    CurrentPoint = enumerator.Current;

                    HasNext = enumerator.MoveNext();

                    CurrentIsBroken = double.IsNaN(CurrentPoint.X) || double.IsNaN(CurrentPoint.Y);



                    if (CurrentIsBroken || !HasNext)
                    {
                        if (list.Collection.Count > 0)
                        {
                            if (!CurrentIsBroken && !HasNext)
                            {
                                list.Collection.Add(CurrentPoint);
                            }
                            transformedPoints = transform.DataToScreen(list.GetPoints());

                            FakePointLists.Add(new FakePointList(FilterPoints(transformedPoints),
                                            output.Left, output.Right));

                            Offset = new Vector();

                            list = new ObservableDataSource<Point>();
                        }
                        CurrentIsBroken = false;
                    }
                    else
                    {
                        list.Collection.Add(CurrentPoint);
                    }
                }
            }
            else
            {
                double left = output.Left;
                double right = output.Right;
                double shift = Offset.X;
                left -= shift;
                right -= shift;

                foreach (var filteredPoints in FakePointLists)
                {
                    filteredPoints.SetXBorders(left, right);
                }
            }
        }
        #endregion

        #region OnRenderCore
        protected override void OnRenderCore(DrawingContext dc, RenderState state)
        {
            if (DataSource == null) return;

            if (FakePointLists != null)
            {
                StreamGeometry geometry = new StreamGeometry();
                using (StreamGeometryContext context = geometry.Open())
                {
                    foreach (var filteredPoints in FakePointLists)
                    {
                        if(filteredPoints.HasPoints)
                        {
                            context.BeginFigure(filteredPoints.StartPoint, false, false);
                            context.PolyLineTo(filteredPoints, true, true);
                        }
                    }
                }
                geometry.Freeze();

                const Brush brush = null;

                Pen pen = LinePen;
                
                bool isTranslated = IsTranslated;

                if (isTranslated)
                {
                    dc.PushTransform(new TranslateTransform(Offset.X, Offset.Y));
                }

                dc.DrawGeometry(brush, pen, geometry);

                if (isTranslated)
                {
                    dc.Pop();
                }
            }
        }
        #endregion 

        #endregion

        #region Line Pen

        #region Stroke
        /// <summary>
        /// Gets or sets the brush, using which polyline is plotted.
        /// </summary>
        /// <value>The line brush.</value>
        public Brush Stroke
        {
            get { return LinePen.Brush; }
            set
            {
                if (LinePen.Brush != value)
                {
                    if (!LinePen.IsSealed)
                    {
                        LinePen.Brush = value;
                        InvalidateVisual();
                    }
                    else
                    {
                        Pen pen = LinePen.Clone();
                        pen.Brush = value;
                        LinePen = pen;
                    }
                }
            }
        }
        #endregion

        #region StrokeThickness
        /// <summary>
        /// Gets or sets the line thickness.
        /// </summary>
        /// <value>The line thickness.</value>
        public double StrokeThickness
        {
            get { return LinePen.Thickness; }
            set
            {
                if (LinePen.Thickness != value)
                {
                    if (!LinePen.IsSealed)
                    {
                        LinePen.Thickness = value; InvalidateVisual();
                    }
                    else
                    {
                        Pen pen = LinePen.Clone();
                        pen.Thickness = value;
                        LinePen = pen;
                    }
                }
            }
        }
        #endregion

        #region LinePen
        /// <summary>
        /// Gets or sets the line pen.
        /// </summary>
        /// <value>The line pen.</value>
        [NotNull]
        public Pen LinePen
        {
            get { return (Pen)GetValue(LinePenProperty); }
            set { SetValue(LinePenProperty, value); }
        }

        public static readonly DependencyProperty LinePenProperty =
            DependencyProperty.Register(
            "LinePen",
            typeof(Pen),
            typeof(BrokenLineGraph),
            new FrameworkPropertyMetadata(
                new Pen(Brushes.Blue, 1),
                FrameworkPropertyMetadataOptions.AffectsRender
                ),
            OnValidatePen);
        #endregion

        #region OnValidatePen
        private static bool OnValidatePen(object value)
        {
            return value != null;
        }
        #endregion

        #endregion

        #region CreateDefaultDescription
        protected override Description CreateDefaultDescription()
        {
            return new PenDescription();
        }
        #endregion
    }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值