From 2bbedc017858a6eed00a99883b3b6accc50445e5 Mon Sep 17 00:00:00 2001 From: Sean Kessler Date: Fri, 23 Feb 2024 00:46:06 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 330 +++++ AssemblyInfo.cs | 42 + Changelog.txt | 1 + ChartPlotter.cs | 503 +++++++ Charts/Axes/AxisBase.cs | 411 ++++++ Charts/Axes/AxisControl.cs | 1231 +++++++++++++++++ Charts/Axes/AxisControlBase.cs | 42 + Charts/Axes/AxisControlStyle.xaml | 76 + Charts/Axes/AxisGrid.cs | 287 ++++ Charts/Axes/AxisPlacement.cs | 30 + Charts/Axes/DateTime/DateTimeAxis.cs | 113 ++ Charts/Axes/DateTime/DateTimeAxisControl.cs | 27 + Charts/Axes/DateTime/DateTimeLabelProvider.cs | 48 + .../DateTime/DateTimeLabelProviderBase.cs | 59 + Charts/Axes/DateTime/DateTimeTicksProvider.cs | 307 ++++ .../DateTime/DateTimeTicksProviderBase.cs | 134 ++ .../DateTime/DateTimeToDoubleConversion.cs | 39 + Charts/Axes/DateTime/DifferenceIn.cs | 22 + .../Axes/DateTime/HorizontalDateTimeAxis.cs | 28 + .../DateTime/MajorDateTimeLabelProvider.cs | 133 ++ Charts/Axes/DateTime/MinorTimeProviderBase.cs | 114 ++ .../DefaultDateTimeTicksStrategy.cs | 59 + .../DateTime/Strategies/DelegateStrategy.cs | 30 + .../Strategies/ExtendedDaysStrategy.cs | 67 + .../Strategies/IDateTimeTicksStrategy.cs | 14 + .../Axes/DateTime/TimePeriodTicksProvider.cs | 269 ++++ Charts/Axes/DateTime/VerticalDateTimeAxis.cs | 23 + Charts/Axes/DateTimeTicksProvider.cs | 495 +++++++ Charts/Axes/DateTimeTicksProviderBase.cs | 145 ++ Charts/Axes/DefaultAxisConversions.cs | 109 ++ Charts/Axes/DefaultNumericTicksProvider.cs | 99 ++ Charts/Axes/DefaultTicksProvider.cs | 17 + Charts/Axes/GeneralAxis.cs | 142 ++ Charts/Axes/GenericLabelProvider.cs | 55 + .../GenericLocationalLabelProvider.cs | 63 + .../GenericLocationalTicksProvider.cs | 146 ++ Charts/Axes/ITicksProvider.cs | 153 ++ Charts/Axes/ITypedAxis.cs | 40 + Charts/Axes/IValueConversion.cs | 13 + .../Axes/Integer/CollectionLabelProvider.cs | 107 ++ Charts/Axes/Integer/HorizontalIntegerAxis.cs | 21 + Charts/Axes/Integer/IntegerAxis.cs | 18 + Charts/Axes/Integer/IntegerAxisControl.cs | 18 + Charts/Axes/Integer/IntegerTicksProvider.cs | 189 +++ Charts/Axes/Integer/VerticalIntegerAxis.cs | 21 + Charts/Axes/LabelProvider.cs | 43 + Charts/Axes/LabelProviderBase.cs | 212 +++ Charts/Axes/LabelProviderProperties.cs | 27 + .../Numeric/CustomBaseNumericLabelProvider.cs | 92 ++ .../Numeric/CustomBaseNumericTicksProvider.cs | 202 +++ .../Axes/Numeric/ExponentialLabelProvider.cs | 92 ++ Charts/Axes/Numeric/HorizontalAxis.cs | 34 + .../Numeric/LogarithmNumericTicksProvider.cs | 131 ++ .../Axes/Numeric/MinorNumericTicksProvider.cs | 89 ++ Charts/Axes/Numeric/NumericAxis.cs | 41 + Charts/Axes/Numeric/NumericAxisControl.cs | 18 + Charts/Axes/Numeric/NumericConversion.cs | 38 + .../Axes/Numeric/NumericLabelProviderBase.cs | 54 + Charts/Axes/Numeric/NumericTicksProvider.cs | 167 +++ Charts/Axes/Numeric/ToStringLabelProvider.cs | 53 + .../Axes/Numeric/UnroundingLabelProvider.cs | 11 + Charts/Axes/Numeric/VerticalAxis.cs | 35 + Charts/Axes/Numeric/VerticalNumericAxis.cs | 21 + Charts/Axes/RoundingHelper.cs | 67 + Charts/Axes/StackCanvas.cs | 237 ++++ .../Axes/TimeSpan/HorizontalTimeSpanAxis.cs | 27 + Charts/Axes/TimeSpan/MinorTimeSpanProvider.cs | 17 + Charts/Axes/TimeSpan/TimeSpanAxis.cs | 57 + Charts/Axes/TimeSpan/TimeSpanAxisControl.cs | 20 + Charts/Axes/TimeSpan/TimeSpanLabelProvider.cs | 33 + Charts/Axes/TimeSpan/TimeSpanTicksProvider.cs | 34 + .../TimeSpan/TimeSpanTicksProviderBase.cs | 319 +++++ .../TimeSpan/TimeSpanToDoubleConversion.cs | 44 + Charts/Axes/TimeSpan/TimeTicksProviderBase.cs | 159 +++ Charts/Axes/TimeSpan/VerticalTimeSpanAxis.cs | 32 + Charts/BackgroundRenderer.cs | 53 + Charts/BitmapBasedGraph.cs | 341 +++++ Charts/ContentGraph.cs | 89 ++ Charts/DataFollowChart.cs | 361 +++++ Charts/DataSource2dContext.cs | 45 + Charts/DebugMenu.cs | 61 + Charts/FakePointList.cs | 136 ++ Charts/FilterCollection.cs | 49 + Charts/Filters/EmptyFilter.cs | 16 + Charts/Filters/FrequencyFilter.cs | 136 ++ Charts/Filters/FrequencyFilter2.cs | 90 ++ Charts/Filters/IPointsFilter.cs | 23 + Charts/Filters/InclinationFilter.cs | 74 + Charts/Filters/PointsFilterBase.cs | 26 + Charts/IOneDimensionalChart.cs | 14 + Charts/Isolines/AdditionalLinesRenderer.cs | 87 ++ Charts/Isolines/CellInfo.cs | 399 ++++++ Charts/Isolines/Enums.cs | 67 + Charts/Isolines/FastIsolineDisplay.xaml | 19 + Charts/Isolines/FastIsolineDisplay.xaml.cs | 59 + Charts/Isolines/FastIsolineRenderer.cs | 200 +++ Charts/Isolines/IsolineBuilder.cs | 708 ++++++++++ Charts/Isolines/IsolineCollection.cs | 159 +++ Charts/Isolines/IsolineGenerationException.cs | 20 + Charts/Isolines/IsolineGraph.cs | 258 ++++ Charts/Isolines/IsolineGraphBase.cs | 282 ++++ Charts/Isolines/IsolineRenderer.cs | 86 ++ Charts/Isolines/IsolineTextAnnotater.cs | 74 + Charts/Isolines/IsolineTrackingGraph.xaml | 19 + Charts/Isolines/IsolineTrackingGraph.xaml.cs | 255 ++++ Charts/Isolines/Quad.cs | 63 + .../LegendBottomButtonIsEnabledConverter.cs | 22 + Charts/Legend items/LegendItemsHelper.cs | 29 + Charts/Legend items/LegendResources.xaml | 128 ++ Charts/Legend items/LegendStyles.cs | 47 + .../LegendTopButtonToIsEnabledConverter.cs | 18 + Charts/Legend items/NewLegend.cs | 342 +++++ Charts/Legend items/NewLegendItem.cs | 33 + Charts/Legend.xaml | 22 + Charts/Legend.xaml.cs | 301 ++++ Charts/LegendItem.cs | 39 + Charts/LineAndMarker.cs | 13 + Charts/LineGraph.cs | 333 +++++ Charts/LineLegendItem.xaml | 19 + Charts/LineLegendItem.xaml.cs | 18 + Charts/LiveTooltips/LiveTooltip.cs | 51 + Charts/LiveTooltips/LiveTooltipAdorner.cs | 126 ++ Charts/LiveTooltips/LiveTooltipService.cs | 217 +++ Charts/MagnifyingGlass.xaml | 26 + Charts/MagnifyingGlass.xaml.cs | 137 ++ Charts/MarkerElementPointGraph.cs | 140 ++ Charts/MarkerPointGraph.cs | 62 + Charts/MarkerPointsGraph.cs | 76 + Charts/Markers/BarChart.cs | 54 + Charts/Markers/BarFromValueConverter.cs | 57 + Charts/Markers/BindMarkerInfo.cs | 16 + Charts/Markers/MarkerChart.cs | 286 ++++ Charts/Markers/OldMarkerGenerator.cs | 26 + Charts/Markers/TemplateMarkerGenerator2.cs | 55 + Charts/MarkersPointsGraph.cs | 78 ++ Charts/NaiveColorMap.cs | 60 + Charts/Navigation/AboutWindow.xaml | 128 ++ Charts/Navigation/AboutWindow.xaml.cs | 50 + Charts/Navigation/AxisCursorGraph.cs | 222 +++ Charts/Navigation/AxisNavigation.cs | 405 ++++++ Charts/Navigation/ChartCommands.cs | 170 +++ Charts/Navigation/CursorCoordinateGraph.xaml | 59 + .../Navigation/CursorCoordinateGraph.xaml.cs | 470 +++++++ Charts/Navigation/DefaultContextMenu.cs | 331 +++++ Charts/Navigation/EndlessRectAnimation.cs | 49 + Charts/Navigation/HorizontalScrollBar.cs | 59 + .../Navigation/IPlotterContextMenuSource.cs | 12 + Charts/Navigation/InertialMouseNavigation.cs | 94 ++ Charts/Navigation/KeyboardNavigation.cs | 454 ++++++ Charts/Navigation/LongOperationsIndicator.cs | 144 ++ .../LongOperationsIndicatorResources.xaml | 11 + Charts/Navigation/MessagesHelper.cs | 32 + Charts/Navigation/MouseNavigation.cs | 406 ++++++ Charts/Navigation/MouseNavigationBase.cs | 63 + Charts/Navigation/Navigation/ChartCommands.cs | 103 ++ .../Navigation/DefaultContextMenu.cs | 91 ++ .../Navigation/InertialMouseNavigation.cs | 317 +++++ .../Navigation/KeyboardNavigation.cs | 300 ++++ .../Navigation/Navigation/MessagesHelper.cs | 32 + .../Navigation/Navigation/MouseNavigation.cs | 235 ++++ .../Navigation/Navigation/NavigationBase.cs | 25 + .../Navigation/RectangleSelectionAdorner.cs | 48 + .../Navigation/Navigation/TouchPadScroll.cs | 52 + .../Navigation/Navigation/WindowsMessages.cs | 7 + Charts/Navigation/NavigationBase.cs | 34 + Charts/Navigation/OldAxisNavigation.cs | 205 +++ Charts/Navigation/PhysicalNavigation.cs | 120 ++ Charts/Navigation/PhysicalRectAnimation.cs | 173 +++ Charts/Navigation/PlotterScrollBar.cs | 96 ++ .../Navigation/RectangleSelectionAdorner.cs | 48 + Charts/Navigation/SimpleNavigationBar.xaml | 21 + Charts/Navigation/SimpleNavigationBar.xaml.cs | 27 + Charts/Navigation/TouchPadScroll.cs | 52 + Charts/Navigation/VerticalScrollBar.cs | 68 + Charts/Navigation/WindowsMessages.cs | 7 + Charts/PointGraphBase.cs | 210 +++ Charts/PopupTip.cs | 53 + Charts/RemoveAll.cs | 46 + Charts/Shapes/Arrow.cs | 124 ++ Charts/Shapes/DraggablePoint.xaml | 46 + Charts/Shapes/DraggablePoint.xaml.cs | 97 ++ Charts/Shapes/HorizontalLine.cs | 42 + Charts/Shapes/HorizontalRange.cs | 35 + Charts/Shapes/PolylineEditor.cs | 142 ++ .../Shapes/PositionalViewportUIContainer.cs | 194 +++ Charts/Shapes/RangeHighlight.cs | 303 ++++ Charts/Shapes/RangeHighlightStyle.xaml | 46 + Charts/Shapes/RectangleHighlight.cs | 59 + Charts/Shapes/Segment.cs | 104 ++ Charts/Shapes/SimpleLine.cs | 62 + Charts/Shapes/TemplateableDraggablePoint.cs | 33 + Charts/Shapes/VerticalLine.cs | 41 + Charts/Shapes/VerticalRange.cs | 34 + Charts/Shapes/ViewportPolyBezierCurve.cs | 98 ++ Charts/Shapes/ViewportPolygon.cs | 59 + Charts/Shapes/ViewportPolyline.cs | 51 + Charts/Shapes/ViewportPolylineBase.cs | 75 + Charts/Shapes/ViewportShape.cs | 82 ++ Charts/ViewportHostPanel.cs | 317 +++++ Charts/ViewportPanel.Properties.cs | 297 ++++ Charts/ViewportPanel.cs | 284 ++++ Charts/ViewportUIContainer.cs | 101 ++ Common/Auxiliary/ArrayExtensions.cs | 36 + Common/Auxiliary/BindingHelper.cs | 17 + Common/Auxiliary/BoundsHelper.cs | 46 + Common/Auxiliary/BrushHelper.cs | 69 + Common/Auxiliary/ColorExtensions.cs | 22 + Common/Auxiliary/ColorHelper.cs | 104 ++ Common/Auxiliary/CoordinateUtils.cs | 76 + Common/Auxiliary/DataRectExtensions.cs | 82 ++ .../Auxiliary/DataSearch/GenericSearcher1d.cs | 95 ++ Common/Auxiliary/DataSearch/SearchResult1d.cs | 30 + .../Auxiliary/DataSearch/SortedXSearcher1d.cs | 56 + Common/Auxiliary/DebugVerify.cs | 59 + .../Auxiliary/DependencyObjectExtensions.cs | 20 + Common/Auxiliary/DictionaryExtensions.cs | 27 + Common/Auxiliary/DispatcherExtensions.cs | 26 + Common/Auxiliary/DisposableTimer.cs | 43 + Common/Auxiliary/DoubleCollectionHelper.cs | 16 + Common/Auxiliary/EventExtensions.cs | 140 ++ Common/Auxiliary/HsbColor.cs | 301 ++++ Common/Auxiliary/IDataSource2DExtensions.cs | 146 ++ Common/Auxiliary/IEnumerableExtensions.cs | 119 ++ Common/Auxiliary/IListExtensions.cs | 44 + Common/Auxiliary/IPlotterElementExtensions.cs | 32 + .../Auxiliary/IPointCollectionExtensions.cs | 21 + Common/Auxiliary/ListExtensions.cs | 34 + Common/Auxiliary/ListGenerator.cs | 26 + .../MarkupExtensions/ResourceExtension.cs | 53 + .../Auxiliary/MarkupExtensions/SelfBinding.cs | 23 + .../MarkupExtensions/TemplateBinding.cs | 22 + .../XbapConditionalExpression.cs | 33 + Common/Auxiliary/MathHelper.cs | 91 ++ Common/Auxiliary/MenuItemExtensions.cs | 16 + .../Auxiliary/ObservableCollectionHelper.cs | 35 + Common/Auxiliary/PlacementExtensions.cs | 16 + .../PlotterChildrenCollectionExtensions.cs | 32 + Common/Auxiliary/PlotterExtensions.cs | 15 + Common/Auxiliary/PointExtensions.cs | 19 + Common/Auxiliary/RandomExtensions.cs | 15 + Common/Auxiliary/RangeExtensions.cs | 25 + Common/Auxiliary/RectExtensions.cs | 67 + Common/Auxiliary/ResourcePoolExtensions.cs | 22 + Common/Auxiliary/ScreenshotHelper.cs | 205 +++ Common/Auxiliary/SizeExtensions.cs | 20 + Common/Auxiliary/SizeHelper.cs | 16 + Common/Auxiliary/StreamExtensions.cs | 23 + Common/Auxiliary/StringExtensions.cs | 20 + Common/Auxiliary/TaskExtensions.cs | 50 + Common/Auxiliary/TriangleMath.cs | 73 + Common/Auxiliary/ValueStore.cs | 106 ++ Common/Auxiliary/ValueStoreConverter.cs | 30 + Common/Auxiliary/VectorExtensions.cs | 16 + Common/Auxiliary/Verify.cs | 64 + Common/Auxiliary/VisualTreeHelperHelper.cs | 27 + Common/BezierBuilder.cs | 120 ++ Common/D3Collection.cs | 132 ++ Common/DataRect.cs | 687 +++++++++ Common/DataRectConverter.cs | 51 + Common/DataRectSerializer.cs | 41 + Common/Footer.cs | 42 + Common/Header.cs | 42 + Common/HorizontalAxisTitle.cs | 101 ++ Common/IndividualArrangePanel.cs | 111 ++ Common/NotNullAttribute.cs | 14 + Common/NotifyingPanels/INotifyingPanel.cs | 13 + Common/NotifyingPanels/NotifyingCanvas.cs | 32 + Common/NotifyingPanels/NotifyingGrid.cs | 32 + Common/NotifyingPanels/NotifyingStackPanel.cs | 37 + .../NotifyingUIElementCollection.cs | 106 ++ Common/ObservableCollectionWrapper.cs | 175 +++ Common/ObservableUIElementCollection.cs | 24 + Common/Palettes/DecoratorPaletteBase.cs | 66 + Common/Palettes/DelegatePalette.cs | 26 + Common/Palettes/HsbPalette.cs | 61 + Common/Palettes/IPalette.cs | 26 + Common/Palettes/LinearPalette.cs | 99 ++ Common/Palettes/LinearPalettes.cs | 30 + Common/Palettes/MinMaxLoggingPalette.cs | 33 + Common/Palettes/PaletteBase.cs | 35 + Common/Palettes/TransparentLimitsPalette.cs | 24 + Common/Palettes/UniformLinearPalette.cs | 133 ++ Common/Palettes/UniformLinearPalettes.cs | 45 + Common/Plotter.cs | 952 +++++++++++++ Common/Plotter.xaml.cs | 206 +++ Common/PlotterAutomationPeer.cs | 27 + Common/PlotterChangedEventHandler.cs | 28 + Common/PlotterChildrenCollection.cs | 209 +++ Common/PlotterElement.cs | 87 ++ Common/PlotterEventHelper.cs | 39 + Common/PlotterEvents.cs | 36 + Common/PlotterPanel.cs | 12 + Common/PlotterStyle.xaml | 84 ++ Common/Range.cs | 150 ++ Common/RangeConverter.cs | 74 + Common/RenderState.cs | 56 + Common/ResourcePool.cs | 56 + Common/RingArray.cs | 140 ++ Common/SkipPropertyCheckAttribute.cs | 14 + Common/TokenizerHelper.cs | 24 + Common/UIChildrenCollection.cs | 211 +++ Common/UndoSystem/ActionStack.cs | 51 + Common/UndoSystem/CollectionAddAction.cs | 34 + Common/UndoSystem/CollectionRemoveAction.cs | 36 + .../DependencyPropertyChangedUndoAction.cs | 42 + Common/UndoSystem/LambdaUndoAction.cs | 34 + Common/UndoSystem/UndoAction.cs | 13 + Common/UndoSystem/UndoProvider.cs | 154 +++ Common/ValueChangedEventArgs.cs | 28 + Common/VerticalAxisTitle.cs | 110 ++ Common/VisualBindingCollection.cs | 30 + Common/WeakReference.cs | 32 + Common/WidthSpring.cs | 97 ++ Converters/BackgroundToForegroundConverter.cs | 24 + Converters/BrushHSBConverter.cs | 47 + Converters/FourValuesMultiConverter.cs | 41 + Converters/GenericValueConverter.cs | 54 + Converters/ThreeValuesMultiConverter.cs | 39 + Converters/TwoValuesMultiConverter.cs | 40 + .../MultiDimensional/DataSource2DHelper.cs | 51 + .../MultiDimensional/EmptyDataSource2D.cs | 75 + DataSources/MultiDimensional/IDataSource2D.cs | 55 + .../INonUniformDataSource2D.cs | 13 + .../NonUniformDataSource2D.cs | 113 ++ .../MultiDimensional/WarpedDataSource2D.cs | 113 ++ .../OneDimensional/CompositeDataSource.cs | 107 ++ .../OneDimensional/DataSourceExtensions.cs | 92 ++ .../OneDimensional/DataSourceHelper.cs | 35 + DataSources/OneDimensional/EmptyDataSource.cs | 55 + .../OneDimensional/EnumerableDataSource.cs | 109 ++ .../EnumerableDataSourceBase.cs | 56 + .../EnumerablePointEnumerator.cs | 36 + .../OneDimensional/EnumerableXDataSource.cs | 12 + .../OneDimensional/EnumerableYDataSource.cs | 12 + .../OneDimensional/IPointDataSource.cs | 16 + .../OneDimensional/IPointEnumerator.cs | 21 + DataSources/OneDimensional/Mapping.cs | 15 + .../OneDimensional/ObservableDataSource.cs | 220 +++ DataSources/OneDimensional/RawDataSource.cs | 14 + .../OneDimensional/RawPointEnumerator.cs | 29 + DataSources/OneDimensional/TableDataSource.cs | 32 + Descriptions/Description.cs | 86 ++ Descriptions/PenDescription.cs | 64 + Descriptions/StandardDescription.cs | 42 + Docs/d3overview.pdf | Bin 0 -> 1002871 bytes DynamicDataDisplay.License.txt | 35 + DynamicDataDisplay.Readme.txt | 8 + DynamicDataDisplay.csproj | 708 ++++++++++ DynamicDataDisplay.sln | 23 + DynamicDataDisplay.snk | Bin 0 -> 596 bytes ExtendedPropertyChangedEventArgs.cs | 22 + GenericChartPlotter.cs | 121 ++ GenericRect.cs | 153 ++ GlobalSuppressions.cs | 15 + InjectedPlotter.cs | 181 +++ InjectedPlotterVerticalSyncConverter.cs | 50 + Plotter2D.cs | 177 +++ Plotter2DExtensions.cs | 249 ++++ PlotterLoadMode.cs | 14 + PointMarkers/CenteredTextMarker.cs | 84 ++ PointMarkers/CircleElementPointMarker.cs | 50 + PointMarkers/CirclePointMarker.cs | 14 + PointMarkers/CompositePointMarker.cs | 49 + PointMarkers/DelegatePointMarker.cs | 24 + PointMarkers/ElementPointMarker.cs | 19 + PointMarkers/PointMarker.cs | 26 + PointMarkers/RectElementPointMarker.cs | 50 + PointMarkers/ShapeElementPointMarker.cs | 78 ++ PointMarkers/ShapePointMarker.cs | 49 + PointMarkers/TrianglePointMarker.cs | 49 + Properties/AssemblyInfo.cs | 39 + Resources/CopyScreenshotIcon.png | Bin 0 -> 1409 bytes Resources/D3-icon-white.ico | Bin 0 -> 1378 bytes Resources/D3-icon.ico | Bin 0 -> 4286 bytes Resources/D3IconHelper.cs | 41 + Resources/FitToViewIcon.png | Bin 0 -> 1289 bytes Resources/HelpIcon.png | Bin 0 -> 1428 bytes Resources/SaveIcon.png | Bin 0 -> 1428 bytes Resources/Toolbox/ChartPlotter.Icon.png | Bin 0 -> 1409 bytes ScaleConverter.cs | 56 + Strings/Exceptions.Designer.cs | 414 ++++++ Strings/Exceptions.resx | 237 ++++ Strings/Exceptions.ru-ru.Designer.cs | 0 Strings/Exceptions.ru-ru.resx | 132 ++ Strings/UIResources.Designer.cs | 333 +++++ Strings/UIResources.resx | 212 +++ Strings/UIResources.ru-ru.Designer.cs | 0 Strings/UIResources.ru-ru.resx | 204 +++ Themes/Generic.xaml | 71 + TimeChartPlotter.cs | 38 + Transforms/CoordinateTransform.cs | 187 +++ Transforms/CoordinateTransformExtensions.cs | 305 ++++ Transforms/DataDomains.cs | 28 + Transforms/DataTransforms.cs | 564 ++++++++ Transforms/Log10Transform.cs | 56 + Transforms/SwapTransform.cs | 41 + Viewport2D.AttachedProperties.cs | 174 +++ Viewport2D.cs | 611 ++++++++ Viewport2DExtensions.cs | 23 + Viewport2DPanningState.cs | 13 + Viewport2dDeferredPanningProxy.cs | 101 ++ ViewportElement2D.cs | 484 +++++++ ViewportRestrictions/DataHeightRestriction.cs | 171 +++ .../DateTimeHorizontalAxisRestriction.cs | 25 + .../DateTimeVerticalAxisRestriction.cs | 25 + ViewportRestrictions/DomainRestriction.cs | 80 ++ .../FollowWidthRestriction.cs | 75 + .../ISupportAttachToViewport.cs | 26 + .../InjectionDelegateRestriction.cs | 40 + .../MaximalSizeRestriction.cs | 63 + .../MinimalSizeRestriction.cs | 48 + .../PhysicalProportionsRestriction.cs | 79 ++ .../ProportionsRestriction.cs | 38 + ViewportRestrictions/RestrictionCollection.cs | 69 + .../ScaleInjectionRestriction.cs | 57 + ViewportRestrictions/ViewportRestriction.cs | 41 + Views/AnalystRatingsView.xaml | 91 ++ Views/AnalystRatingsView.xaml.cs | 27 + Views/BlotterView.xaml | 158 +++ Views/BlotterView.xaml.cs | 27 + Views/BollingerBandView.xaml | 193 +++ Views/BollingerBandView.xaml.cs | 27 + Views/DCFValuationView.xaml | 182 +++ Views/DCFValuationView.xaml.cs | 27 + Views/DividendView.xaml | 103 ++ Views/DividendView.xaml.cs | 27 + Views/ETFHoldingView.xaml | 70 + Views/ETFHoldingView.xaml.cs | 27 + Views/EarningsAnnouncementView.xaml | 73 + Views/EarningsAnnouncementView.xaml.cs | 27 + Views/GainLossView.xaml | 136 ++ Views/GainLossView.xaml.cs | 28 + Views/HeadlinesView.xaml | 88 ++ Views/HeadlinesView.xaml.cs | 28 + Views/HistoricalView.xaml | 445 ++++++ Views/HistoricalView.xaml.cs | 27 + Views/MACDView.xaml | 164 +++ Views/MACDView.xaml.cs | 27 + Views/MomentumView.xaml | 218 +++ Views/MomentumView.xaml.cs | 28 + Views/MovingAverageView.xaml | 87 ++ Views/MovingAverageView.xaml.cs | 27 + Views/OptionsView.xaml | 188 +++ Views/OptionsView.xaml.cs | 27 + Views/OptionsWorksheetView.xaml | 222 +++ Views/OptionsWorksheetView.xaml.cs | 28 + Views/PricingView.xaml | 151 ++ Views/PricingView.xaml.cs | 27 + Views/ResistanceAndSupportView.xaml | 67 + Views/ResistanceAndSupportView.xaml.cs | 27 + Views/SECFilingView.xaml | 53 + Views/SECFilingView.xaml.cs | 27 + Views/SectorView.xaml | 96 ++ Views/SectorView.xaml.cs | 27 + Views/StickerPriceView.xaml | 186 +++ Views/StickerPriceView.xaml.cs | 27 + Views/StochasticsView.xaml | 62 + Views/StochasticsView.xaml.cs | 32 + Views/TradeEntryView.xaml | 210 +++ Views/TradeEntryView.xaml.cs | 27 + Views/TradeModelView.xaml | 99 ++ Views/TradeModelView.xaml.cs | 27 + Views/ValuationsView.xaml | 290 ++++ Views/ValuationsView.xaml.cs | 27 + Views/ValueAtRiskView.xaml | 86 ++ Views/ValueAtRiskView.xaml.cs | 27 + Views/WatchListView.xaml | 80 ++ Views/WatchListView.xaml.cs | 28 + Views/YieldCurveView.xaml | 55 + Views/YieldCurveView.xaml.cs | 27 + 470 files changed, 46035 insertions(+) create mode 100644 .gitignore create mode 100644 AssemblyInfo.cs create mode 100644 Changelog.txt create mode 100644 ChartPlotter.cs create mode 100644 Charts/Axes/AxisBase.cs create mode 100644 Charts/Axes/AxisControl.cs create mode 100644 Charts/Axes/AxisControlBase.cs create mode 100644 Charts/Axes/AxisControlStyle.xaml create mode 100644 Charts/Axes/AxisGrid.cs create mode 100644 Charts/Axes/AxisPlacement.cs create mode 100644 Charts/Axes/DateTime/DateTimeAxis.cs create mode 100644 Charts/Axes/DateTime/DateTimeAxisControl.cs create mode 100644 Charts/Axes/DateTime/DateTimeLabelProvider.cs create mode 100644 Charts/Axes/DateTime/DateTimeLabelProviderBase.cs create mode 100644 Charts/Axes/DateTime/DateTimeTicksProvider.cs create mode 100644 Charts/Axes/DateTime/DateTimeTicksProviderBase.cs create mode 100644 Charts/Axes/DateTime/DateTimeToDoubleConversion.cs create mode 100644 Charts/Axes/DateTime/DifferenceIn.cs create mode 100644 Charts/Axes/DateTime/HorizontalDateTimeAxis.cs create mode 100644 Charts/Axes/DateTime/MajorDateTimeLabelProvider.cs create mode 100644 Charts/Axes/DateTime/MinorTimeProviderBase.cs create mode 100644 Charts/Axes/DateTime/Strategies/DefaultDateTimeTicksStrategy.cs create mode 100644 Charts/Axes/DateTime/Strategies/DelegateStrategy.cs create mode 100644 Charts/Axes/DateTime/Strategies/ExtendedDaysStrategy.cs create mode 100644 Charts/Axes/DateTime/Strategies/IDateTimeTicksStrategy.cs create mode 100644 Charts/Axes/DateTime/TimePeriodTicksProvider.cs create mode 100644 Charts/Axes/DateTime/VerticalDateTimeAxis.cs create mode 100644 Charts/Axes/DateTimeTicksProvider.cs create mode 100644 Charts/Axes/DateTimeTicksProviderBase.cs create mode 100644 Charts/Axes/DefaultAxisConversions.cs create mode 100644 Charts/Axes/DefaultNumericTicksProvider.cs create mode 100644 Charts/Axes/DefaultTicksProvider.cs create mode 100644 Charts/Axes/GeneralAxis.cs create mode 100644 Charts/Axes/GenericLabelProvider.cs create mode 100644 Charts/Axes/GenericLocational/GenericLocationalLabelProvider.cs create mode 100644 Charts/Axes/GenericLocational/GenericLocationalTicksProvider.cs create mode 100644 Charts/Axes/ITicksProvider.cs create mode 100644 Charts/Axes/ITypedAxis.cs create mode 100644 Charts/Axes/IValueConversion.cs create mode 100644 Charts/Axes/Integer/CollectionLabelProvider.cs create mode 100644 Charts/Axes/Integer/HorizontalIntegerAxis.cs create mode 100644 Charts/Axes/Integer/IntegerAxis.cs create mode 100644 Charts/Axes/Integer/IntegerAxisControl.cs create mode 100644 Charts/Axes/Integer/IntegerTicksProvider.cs create mode 100644 Charts/Axes/Integer/VerticalIntegerAxis.cs create mode 100644 Charts/Axes/LabelProvider.cs create mode 100644 Charts/Axes/LabelProviderBase.cs create mode 100644 Charts/Axes/LabelProviderProperties.cs create mode 100644 Charts/Axes/Numeric/CustomBaseNumericLabelProvider.cs create mode 100644 Charts/Axes/Numeric/CustomBaseNumericTicksProvider.cs create mode 100644 Charts/Axes/Numeric/ExponentialLabelProvider.cs create mode 100644 Charts/Axes/Numeric/HorizontalAxis.cs create mode 100644 Charts/Axes/Numeric/LogarithmNumericTicksProvider.cs create mode 100644 Charts/Axes/Numeric/MinorNumericTicksProvider.cs create mode 100644 Charts/Axes/Numeric/NumericAxis.cs create mode 100644 Charts/Axes/Numeric/NumericAxisControl.cs create mode 100644 Charts/Axes/Numeric/NumericConversion.cs create mode 100644 Charts/Axes/Numeric/NumericLabelProviderBase.cs create mode 100644 Charts/Axes/Numeric/NumericTicksProvider.cs create mode 100644 Charts/Axes/Numeric/ToStringLabelProvider.cs create mode 100644 Charts/Axes/Numeric/UnroundingLabelProvider.cs create mode 100644 Charts/Axes/Numeric/VerticalAxis.cs create mode 100644 Charts/Axes/Numeric/VerticalNumericAxis.cs create mode 100644 Charts/Axes/RoundingHelper.cs create mode 100644 Charts/Axes/StackCanvas.cs create mode 100644 Charts/Axes/TimeSpan/HorizontalTimeSpanAxis.cs create mode 100644 Charts/Axes/TimeSpan/MinorTimeSpanProvider.cs create mode 100644 Charts/Axes/TimeSpan/TimeSpanAxis.cs create mode 100644 Charts/Axes/TimeSpan/TimeSpanAxisControl.cs create mode 100644 Charts/Axes/TimeSpan/TimeSpanLabelProvider.cs create mode 100644 Charts/Axes/TimeSpan/TimeSpanTicksProvider.cs create mode 100644 Charts/Axes/TimeSpan/TimeSpanTicksProviderBase.cs create mode 100644 Charts/Axes/TimeSpan/TimeSpanToDoubleConversion.cs create mode 100644 Charts/Axes/TimeSpan/TimeTicksProviderBase.cs create mode 100644 Charts/Axes/TimeSpan/VerticalTimeSpanAxis.cs create mode 100644 Charts/BackgroundRenderer.cs create mode 100644 Charts/BitmapBasedGraph.cs create mode 100644 Charts/ContentGraph.cs create mode 100644 Charts/DataFollowChart.cs create mode 100644 Charts/DataSource2dContext.cs create mode 100644 Charts/DebugMenu.cs create mode 100644 Charts/FakePointList.cs create mode 100644 Charts/FilterCollection.cs create mode 100644 Charts/Filters/EmptyFilter.cs create mode 100644 Charts/Filters/FrequencyFilter.cs create mode 100644 Charts/Filters/FrequencyFilter2.cs create mode 100644 Charts/Filters/IPointsFilter.cs create mode 100644 Charts/Filters/InclinationFilter.cs create mode 100644 Charts/Filters/PointsFilterBase.cs create mode 100644 Charts/IOneDimensionalChart.cs create mode 100644 Charts/Isolines/AdditionalLinesRenderer.cs create mode 100644 Charts/Isolines/CellInfo.cs create mode 100644 Charts/Isolines/Enums.cs create mode 100644 Charts/Isolines/FastIsolineDisplay.xaml create mode 100644 Charts/Isolines/FastIsolineDisplay.xaml.cs create mode 100644 Charts/Isolines/FastIsolineRenderer.cs create mode 100644 Charts/Isolines/IsolineBuilder.cs create mode 100644 Charts/Isolines/IsolineCollection.cs create mode 100644 Charts/Isolines/IsolineGenerationException.cs create mode 100644 Charts/Isolines/IsolineGraph.cs create mode 100644 Charts/Isolines/IsolineGraphBase.cs create mode 100644 Charts/Isolines/IsolineRenderer.cs create mode 100644 Charts/Isolines/IsolineTextAnnotater.cs create mode 100644 Charts/Isolines/IsolineTrackingGraph.xaml create mode 100644 Charts/Isolines/IsolineTrackingGraph.xaml.cs create mode 100644 Charts/Isolines/Quad.cs create mode 100644 Charts/Legend items/LegendBottomButtonIsEnabledConverter.cs create mode 100644 Charts/Legend items/LegendItemsHelper.cs create mode 100644 Charts/Legend items/LegendResources.xaml create mode 100644 Charts/Legend items/LegendStyles.cs create mode 100644 Charts/Legend items/LegendTopButtonToIsEnabledConverter.cs create mode 100644 Charts/Legend items/NewLegend.cs create mode 100644 Charts/Legend items/NewLegendItem.cs create mode 100644 Charts/Legend.xaml create mode 100644 Charts/Legend.xaml.cs create mode 100644 Charts/LegendItem.cs create mode 100644 Charts/LineAndMarker.cs create mode 100644 Charts/LineGraph.cs create mode 100644 Charts/LineLegendItem.xaml create mode 100644 Charts/LineLegendItem.xaml.cs create mode 100644 Charts/LiveTooltips/LiveTooltip.cs create mode 100644 Charts/LiveTooltips/LiveTooltipAdorner.cs create mode 100644 Charts/LiveTooltips/LiveTooltipService.cs create mode 100644 Charts/MagnifyingGlass.xaml create mode 100644 Charts/MagnifyingGlass.xaml.cs create mode 100644 Charts/MarkerElementPointGraph.cs create mode 100644 Charts/MarkerPointGraph.cs create mode 100644 Charts/MarkerPointsGraph.cs create mode 100644 Charts/Markers/BarChart.cs create mode 100644 Charts/Markers/BarFromValueConverter.cs create mode 100644 Charts/Markers/BindMarkerInfo.cs create mode 100644 Charts/Markers/MarkerChart.cs create mode 100644 Charts/Markers/OldMarkerGenerator.cs create mode 100644 Charts/Markers/TemplateMarkerGenerator2.cs create mode 100644 Charts/MarkersPointsGraph.cs create mode 100644 Charts/NaiveColorMap.cs create mode 100644 Charts/Navigation/AboutWindow.xaml create mode 100644 Charts/Navigation/AboutWindow.xaml.cs create mode 100644 Charts/Navigation/AxisCursorGraph.cs create mode 100644 Charts/Navigation/AxisNavigation.cs create mode 100644 Charts/Navigation/ChartCommands.cs create mode 100644 Charts/Navigation/CursorCoordinateGraph.xaml create mode 100644 Charts/Navigation/CursorCoordinateGraph.xaml.cs create mode 100644 Charts/Navigation/DefaultContextMenu.cs create mode 100644 Charts/Navigation/EndlessRectAnimation.cs create mode 100644 Charts/Navigation/HorizontalScrollBar.cs create mode 100644 Charts/Navigation/IPlotterContextMenuSource.cs create mode 100644 Charts/Navigation/InertialMouseNavigation.cs create mode 100644 Charts/Navigation/KeyboardNavigation.cs create mode 100644 Charts/Navigation/LongOperationsIndicator.cs create mode 100644 Charts/Navigation/LongOperationsIndicatorResources.xaml create mode 100644 Charts/Navigation/MessagesHelper.cs create mode 100644 Charts/Navigation/MouseNavigation.cs create mode 100644 Charts/Navigation/MouseNavigationBase.cs create mode 100644 Charts/Navigation/Navigation/ChartCommands.cs create mode 100644 Charts/Navigation/Navigation/DefaultContextMenu.cs create mode 100644 Charts/Navigation/Navigation/InertialMouseNavigation.cs create mode 100644 Charts/Navigation/Navigation/KeyboardNavigation.cs create mode 100644 Charts/Navigation/Navigation/MessagesHelper.cs create mode 100644 Charts/Navigation/Navigation/MouseNavigation.cs create mode 100644 Charts/Navigation/Navigation/NavigationBase.cs create mode 100644 Charts/Navigation/Navigation/RectangleSelectionAdorner.cs create mode 100644 Charts/Navigation/Navigation/TouchPadScroll.cs create mode 100644 Charts/Navigation/Navigation/WindowsMessages.cs create mode 100644 Charts/Navigation/NavigationBase.cs create mode 100644 Charts/Navigation/OldAxisNavigation.cs create mode 100644 Charts/Navigation/PhysicalNavigation.cs create mode 100644 Charts/Navigation/PhysicalRectAnimation.cs create mode 100644 Charts/Navigation/PlotterScrollBar.cs create mode 100644 Charts/Navigation/RectangleSelectionAdorner.cs create mode 100644 Charts/Navigation/SimpleNavigationBar.xaml create mode 100644 Charts/Navigation/SimpleNavigationBar.xaml.cs create mode 100644 Charts/Navigation/TouchPadScroll.cs create mode 100644 Charts/Navigation/VerticalScrollBar.cs create mode 100644 Charts/Navigation/WindowsMessages.cs create mode 100644 Charts/PointGraphBase.cs create mode 100644 Charts/PopupTip.cs create mode 100644 Charts/RemoveAll.cs create mode 100644 Charts/Shapes/Arrow.cs create mode 100644 Charts/Shapes/DraggablePoint.xaml create mode 100644 Charts/Shapes/DraggablePoint.xaml.cs create mode 100644 Charts/Shapes/HorizontalLine.cs create mode 100644 Charts/Shapes/HorizontalRange.cs create mode 100644 Charts/Shapes/PolylineEditor.cs create mode 100644 Charts/Shapes/PositionalViewportUIContainer.cs create mode 100644 Charts/Shapes/RangeHighlight.cs create mode 100644 Charts/Shapes/RangeHighlightStyle.xaml create mode 100644 Charts/Shapes/RectangleHighlight.cs create mode 100644 Charts/Shapes/Segment.cs create mode 100644 Charts/Shapes/SimpleLine.cs create mode 100644 Charts/Shapes/TemplateableDraggablePoint.cs create mode 100644 Charts/Shapes/VerticalLine.cs create mode 100644 Charts/Shapes/VerticalRange.cs create mode 100644 Charts/Shapes/ViewportPolyBezierCurve.cs create mode 100644 Charts/Shapes/ViewportPolygon.cs create mode 100644 Charts/Shapes/ViewportPolyline.cs create mode 100644 Charts/Shapes/ViewportPolylineBase.cs create mode 100644 Charts/Shapes/ViewportShape.cs create mode 100644 Charts/ViewportHostPanel.cs create mode 100644 Charts/ViewportPanel.Properties.cs create mode 100644 Charts/ViewportPanel.cs create mode 100644 Charts/ViewportUIContainer.cs create mode 100644 Common/Auxiliary/ArrayExtensions.cs create mode 100644 Common/Auxiliary/BindingHelper.cs create mode 100644 Common/Auxiliary/BoundsHelper.cs create mode 100644 Common/Auxiliary/BrushHelper.cs create mode 100644 Common/Auxiliary/ColorExtensions.cs create mode 100644 Common/Auxiliary/ColorHelper.cs create mode 100644 Common/Auxiliary/CoordinateUtils.cs create mode 100644 Common/Auxiliary/DataRectExtensions.cs create mode 100644 Common/Auxiliary/DataSearch/GenericSearcher1d.cs create mode 100644 Common/Auxiliary/DataSearch/SearchResult1d.cs create mode 100644 Common/Auxiliary/DataSearch/SortedXSearcher1d.cs create mode 100644 Common/Auxiliary/DebugVerify.cs create mode 100644 Common/Auxiliary/DependencyObjectExtensions.cs create mode 100644 Common/Auxiliary/DictionaryExtensions.cs create mode 100644 Common/Auxiliary/DispatcherExtensions.cs create mode 100644 Common/Auxiliary/DisposableTimer.cs create mode 100644 Common/Auxiliary/DoubleCollectionHelper.cs create mode 100644 Common/Auxiliary/EventExtensions.cs create mode 100644 Common/Auxiliary/HsbColor.cs create mode 100644 Common/Auxiliary/IDataSource2DExtensions.cs create mode 100644 Common/Auxiliary/IEnumerableExtensions.cs create mode 100644 Common/Auxiliary/IListExtensions.cs create mode 100644 Common/Auxiliary/IPlotterElementExtensions.cs create mode 100644 Common/Auxiliary/IPointCollectionExtensions.cs create mode 100644 Common/Auxiliary/ListExtensions.cs create mode 100644 Common/Auxiliary/ListGenerator.cs create mode 100644 Common/Auxiliary/MarkupExtensions/ResourceExtension.cs create mode 100644 Common/Auxiliary/MarkupExtensions/SelfBinding.cs create mode 100644 Common/Auxiliary/MarkupExtensions/TemplateBinding.cs create mode 100644 Common/Auxiliary/MarkupExtensions/XbapConditionalExpression.cs create mode 100644 Common/Auxiliary/MathHelper.cs create mode 100644 Common/Auxiliary/MenuItemExtensions.cs create mode 100644 Common/Auxiliary/ObservableCollectionHelper.cs create mode 100644 Common/Auxiliary/PlacementExtensions.cs create mode 100644 Common/Auxiliary/PlotterChildrenCollectionExtensions.cs create mode 100644 Common/Auxiliary/PlotterExtensions.cs create mode 100644 Common/Auxiliary/PointExtensions.cs create mode 100644 Common/Auxiliary/RandomExtensions.cs create mode 100644 Common/Auxiliary/RangeExtensions.cs create mode 100644 Common/Auxiliary/RectExtensions.cs create mode 100644 Common/Auxiliary/ResourcePoolExtensions.cs create mode 100644 Common/Auxiliary/ScreenshotHelper.cs create mode 100644 Common/Auxiliary/SizeExtensions.cs create mode 100644 Common/Auxiliary/SizeHelper.cs create mode 100644 Common/Auxiliary/StreamExtensions.cs create mode 100644 Common/Auxiliary/StringExtensions.cs create mode 100644 Common/Auxiliary/TaskExtensions.cs create mode 100644 Common/Auxiliary/TriangleMath.cs create mode 100644 Common/Auxiliary/ValueStore.cs create mode 100644 Common/Auxiliary/ValueStoreConverter.cs create mode 100644 Common/Auxiliary/VectorExtensions.cs create mode 100644 Common/Auxiliary/Verify.cs create mode 100644 Common/Auxiliary/VisualTreeHelperHelper.cs create mode 100644 Common/BezierBuilder.cs create mode 100644 Common/D3Collection.cs create mode 100644 Common/DataRect.cs create mode 100644 Common/DataRectConverter.cs create mode 100644 Common/DataRectSerializer.cs create mode 100644 Common/Footer.cs create mode 100644 Common/Header.cs create mode 100644 Common/HorizontalAxisTitle.cs create mode 100644 Common/IndividualArrangePanel.cs create mode 100644 Common/NotNullAttribute.cs create mode 100644 Common/NotifyingPanels/INotifyingPanel.cs create mode 100644 Common/NotifyingPanels/NotifyingCanvas.cs create mode 100644 Common/NotifyingPanels/NotifyingGrid.cs create mode 100644 Common/NotifyingPanels/NotifyingStackPanel.cs create mode 100644 Common/NotifyingPanels/NotifyingUIElementCollection.cs create mode 100644 Common/ObservableCollectionWrapper.cs create mode 100644 Common/ObservableUIElementCollection.cs create mode 100644 Common/Palettes/DecoratorPaletteBase.cs create mode 100644 Common/Palettes/DelegatePalette.cs create mode 100644 Common/Palettes/HsbPalette.cs create mode 100644 Common/Palettes/IPalette.cs create mode 100644 Common/Palettes/LinearPalette.cs create mode 100644 Common/Palettes/LinearPalettes.cs create mode 100644 Common/Palettes/MinMaxLoggingPalette.cs create mode 100644 Common/Palettes/PaletteBase.cs create mode 100644 Common/Palettes/TransparentLimitsPalette.cs create mode 100644 Common/Palettes/UniformLinearPalette.cs create mode 100644 Common/Palettes/UniformLinearPalettes.cs create mode 100644 Common/Plotter.cs create mode 100644 Common/Plotter.xaml.cs create mode 100644 Common/PlotterAutomationPeer.cs create mode 100644 Common/PlotterChangedEventHandler.cs create mode 100644 Common/PlotterChildrenCollection.cs create mode 100644 Common/PlotterElement.cs create mode 100644 Common/PlotterEventHelper.cs create mode 100644 Common/PlotterEvents.cs create mode 100644 Common/PlotterPanel.cs create mode 100644 Common/PlotterStyle.xaml create mode 100644 Common/Range.cs create mode 100644 Common/RangeConverter.cs create mode 100644 Common/RenderState.cs create mode 100644 Common/ResourcePool.cs create mode 100644 Common/RingArray.cs create mode 100644 Common/SkipPropertyCheckAttribute.cs create mode 100644 Common/TokenizerHelper.cs create mode 100644 Common/UIChildrenCollection.cs create mode 100644 Common/UndoSystem/ActionStack.cs create mode 100644 Common/UndoSystem/CollectionAddAction.cs create mode 100644 Common/UndoSystem/CollectionRemoveAction.cs create mode 100644 Common/UndoSystem/DependencyPropertyChangedUndoAction.cs create mode 100644 Common/UndoSystem/LambdaUndoAction.cs create mode 100644 Common/UndoSystem/UndoAction.cs create mode 100644 Common/UndoSystem/UndoProvider.cs create mode 100644 Common/ValueChangedEventArgs.cs create mode 100644 Common/VerticalAxisTitle.cs create mode 100644 Common/VisualBindingCollection.cs create mode 100644 Common/WeakReference.cs create mode 100644 Common/WidthSpring.cs create mode 100644 Converters/BackgroundToForegroundConverter.cs create mode 100644 Converters/BrushHSBConverter.cs create mode 100644 Converters/FourValuesMultiConverter.cs create mode 100644 Converters/GenericValueConverter.cs create mode 100644 Converters/ThreeValuesMultiConverter.cs create mode 100644 Converters/TwoValuesMultiConverter.cs create mode 100644 DataSources/MultiDimensional/DataSource2DHelper.cs create mode 100644 DataSources/MultiDimensional/EmptyDataSource2D.cs create mode 100644 DataSources/MultiDimensional/IDataSource2D.cs create mode 100644 DataSources/MultiDimensional/INonUniformDataSource2D.cs create mode 100644 DataSources/MultiDimensional/NonUniformDataSource2D.cs create mode 100644 DataSources/MultiDimensional/WarpedDataSource2D.cs create mode 100644 DataSources/OneDimensional/CompositeDataSource.cs create mode 100644 DataSources/OneDimensional/DataSourceExtensions.cs create mode 100644 DataSources/OneDimensional/DataSourceHelper.cs create mode 100644 DataSources/OneDimensional/EmptyDataSource.cs create mode 100644 DataSources/OneDimensional/EnumerableDataSource.cs create mode 100644 DataSources/OneDimensional/EnumerableDataSourceBase.cs create mode 100644 DataSources/OneDimensional/EnumerablePointEnumerator.cs create mode 100644 DataSources/OneDimensional/EnumerableXDataSource.cs create mode 100644 DataSources/OneDimensional/EnumerableYDataSource.cs create mode 100644 DataSources/OneDimensional/IPointDataSource.cs create mode 100644 DataSources/OneDimensional/IPointEnumerator.cs create mode 100644 DataSources/OneDimensional/Mapping.cs create mode 100644 DataSources/OneDimensional/ObservableDataSource.cs create mode 100644 DataSources/OneDimensional/RawDataSource.cs create mode 100644 DataSources/OneDimensional/RawPointEnumerator.cs create mode 100644 DataSources/OneDimensional/TableDataSource.cs create mode 100644 Descriptions/Description.cs create mode 100644 Descriptions/PenDescription.cs create mode 100644 Descriptions/StandardDescription.cs create mode 100644 Docs/d3overview.pdf create mode 100644 DynamicDataDisplay.License.txt create mode 100644 DynamicDataDisplay.Readme.txt create mode 100644 DynamicDataDisplay.csproj create mode 100644 DynamicDataDisplay.sln create mode 100644 DynamicDataDisplay.snk create mode 100644 ExtendedPropertyChangedEventArgs.cs create mode 100644 GenericChartPlotter.cs create mode 100644 GenericRect.cs create mode 100644 GlobalSuppressions.cs create mode 100644 InjectedPlotter.cs create mode 100644 InjectedPlotterVerticalSyncConverter.cs create mode 100644 Plotter2D.cs create mode 100644 Plotter2DExtensions.cs create mode 100644 PlotterLoadMode.cs create mode 100644 PointMarkers/CenteredTextMarker.cs create mode 100644 PointMarkers/CircleElementPointMarker.cs create mode 100644 PointMarkers/CirclePointMarker.cs create mode 100644 PointMarkers/CompositePointMarker.cs create mode 100644 PointMarkers/DelegatePointMarker.cs create mode 100644 PointMarkers/ElementPointMarker.cs create mode 100644 PointMarkers/PointMarker.cs create mode 100644 PointMarkers/RectElementPointMarker.cs create mode 100644 PointMarkers/ShapeElementPointMarker.cs create mode 100644 PointMarkers/ShapePointMarker.cs create mode 100644 PointMarkers/TrianglePointMarker.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 Resources/CopyScreenshotIcon.png create mode 100644 Resources/D3-icon-white.ico create mode 100644 Resources/D3-icon.ico create mode 100644 Resources/D3IconHelper.cs create mode 100644 Resources/FitToViewIcon.png create mode 100644 Resources/HelpIcon.png create mode 100644 Resources/SaveIcon.png create mode 100644 Resources/Toolbox/ChartPlotter.Icon.png create mode 100644 ScaleConverter.cs create mode 100644 Strings/Exceptions.Designer.cs create mode 100644 Strings/Exceptions.resx create mode 100644 Strings/Exceptions.ru-ru.Designer.cs create mode 100644 Strings/Exceptions.ru-ru.resx create mode 100644 Strings/UIResources.Designer.cs create mode 100644 Strings/UIResources.resx create mode 100644 Strings/UIResources.ru-ru.Designer.cs create mode 100644 Strings/UIResources.ru-ru.resx create mode 100644 Themes/Generic.xaml create mode 100644 TimeChartPlotter.cs create mode 100644 Transforms/CoordinateTransform.cs create mode 100644 Transforms/CoordinateTransformExtensions.cs create mode 100644 Transforms/DataDomains.cs create mode 100644 Transforms/DataTransforms.cs create mode 100644 Transforms/Log10Transform.cs create mode 100644 Transforms/SwapTransform.cs create mode 100644 Viewport2D.AttachedProperties.cs create mode 100644 Viewport2D.cs create mode 100644 Viewport2DExtensions.cs create mode 100644 Viewport2DPanningState.cs create mode 100644 Viewport2dDeferredPanningProxy.cs create mode 100644 ViewportElement2D.cs create mode 100644 ViewportRestrictions/DataHeightRestriction.cs create mode 100644 ViewportRestrictions/DateTimeHorizontalAxisRestriction.cs create mode 100644 ViewportRestrictions/DateTimeVerticalAxisRestriction.cs create mode 100644 ViewportRestrictions/DomainRestriction.cs create mode 100644 ViewportRestrictions/FollowWidthRestriction.cs create mode 100644 ViewportRestrictions/ISupportAttachToViewport.cs create mode 100644 ViewportRestrictions/InjectionDelegateRestriction.cs create mode 100644 ViewportRestrictions/MaximalSizeRestriction.cs create mode 100644 ViewportRestrictions/MinimalSizeRestriction.cs create mode 100644 ViewportRestrictions/PhysicalProportionsRestriction.cs create mode 100644 ViewportRestrictions/ProportionsRestriction.cs create mode 100644 ViewportRestrictions/RestrictionCollection.cs create mode 100644 ViewportRestrictions/ScaleInjectionRestriction.cs create mode 100644 ViewportRestrictions/ViewportRestriction.cs create mode 100644 Views/AnalystRatingsView.xaml create mode 100644 Views/AnalystRatingsView.xaml.cs create mode 100644 Views/BlotterView.xaml create mode 100644 Views/BlotterView.xaml.cs create mode 100644 Views/BollingerBandView.xaml create mode 100644 Views/BollingerBandView.xaml.cs create mode 100644 Views/DCFValuationView.xaml create mode 100644 Views/DCFValuationView.xaml.cs create mode 100644 Views/DividendView.xaml create mode 100644 Views/DividendView.xaml.cs create mode 100644 Views/ETFHoldingView.xaml create mode 100644 Views/ETFHoldingView.xaml.cs create mode 100644 Views/EarningsAnnouncementView.xaml create mode 100644 Views/EarningsAnnouncementView.xaml.cs create mode 100644 Views/GainLossView.xaml create mode 100644 Views/GainLossView.xaml.cs create mode 100644 Views/HeadlinesView.xaml create mode 100644 Views/HeadlinesView.xaml.cs create mode 100644 Views/HistoricalView.xaml create mode 100644 Views/HistoricalView.xaml.cs create mode 100644 Views/MACDView.xaml create mode 100644 Views/MACDView.xaml.cs create mode 100644 Views/MomentumView.xaml create mode 100644 Views/MomentumView.xaml.cs create mode 100644 Views/MovingAverageView.xaml create mode 100644 Views/MovingAverageView.xaml.cs create mode 100644 Views/OptionsView.xaml create mode 100644 Views/OptionsView.xaml.cs create mode 100644 Views/OptionsWorksheetView.xaml create mode 100644 Views/OptionsWorksheetView.xaml.cs create mode 100644 Views/PricingView.xaml create mode 100644 Views/PricingView.xaml.cs create mode 100644 Views/ResistanceAndSupportView.xaml create mode 100644 Views/ResistanceAndSupportView.xaml.cs create mode 100644 Views/SECFilingView.xaml create mode 100644 Views/SECFilingView.xaml.cs create mode 100644 Views/SectorView.xaml create mode 100644 Views/SectorView.xaml.cs create mode 100644 Views/StickerPriceView.xaml create mode 100644 Views/StickerPriceView.xaml.cs create mode 100644 Views/StochasticsView.xaml create mode 100644 Views/StochasticsView.xaml.cs create mode 100644 Views/TradeEntryView.xaml create mode 100644 Views/TradeEntryView.xaml.cs create mode 100644 Views/TradeModelView.xaml create mode 100644 Views/TradeModelView.xaml.cs create mode 100644 Views/ValuationsView.xaml create mode 100644 Views/ValuationsView.xaml.cs create mode 100644 Views/ValueAtRiskView.xaml create mode 100644 Views/ValueAtRiskView.xaml.cs create mode 100644 Views/WatchListView.xaml create mode 100644 Views/WatchListView.xaml.cs create mode 100644 Views/YieldCurveView.xaml create mode 100644 Views/YieldCurveView.xaml.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e759b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,330 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..6248214 --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,42 @@ +using System; +using System.Resources; +using System.Windows.Markup; +using System.Runtime.CompilerServices; +using Microsoft.Research.DynamicDataDisplay; +using System.Diagnostics.CodeAnalysis; +using System.Security; + +[module: SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")] + +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Navigation")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Charts")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Charts.Navigation")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.DataSources")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Common.Palettes")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Charts.Axes")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Charts.Axes.Numeric")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.PointMarkers")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Charts.Shapes")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Charts.Markers")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Converters")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.MarkupExtensions")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.Charts.Isolines")] +[assembly: XmlnsDefinition(D3AssemblyConstants.DefaultXmlNamespace, "Microsoft.Research.DynamicDataDisplay.ViewportRestrictions")] + +[assembly: XmlnsPrefix(D3AssemblyConstants.DefaultXmlNamespace, "d3")] + +[assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("DynamicDataDisplay.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010039f88065585acdedaac491218a8836c4c54070b4b0f85bc909bd002856b509349f95fc845d1d4c664ea6b93045f2ada3b4fe70c6cd9b3fb615f94b8b5f67e4ea8ea5decb233e2e3c0ce84b78dc3ca0cd9fd2260792ece12224fca5813f03c7ad57b1faa07e3ca8fafb278fa23976fc7a35b8b4ae4efedacd1e193d89738ac2aa")] +[assembly: InternalsVisibleTo("DynamicDataDisplay.Maps, PublicKey=002400000480000094000000060200000024000052534131000400000100010039f88065585acdedaac491218a8836c4c54070b4b0f85bc909bd002856b509349f95fc845d1d4c664ea6b93045f2ada3b4fe70c6cd9b3fb615f94b8b5f67e4ea8ea5decb233e2e3c0ce84b78dc3ca0cd9fd2260792ece12224fca5813f03c7ad57b1faa07e3ca8fafb278fa23976fc7a35b8b4ae4efedacd1e193d89738ac2aa")] +[assembly: InternalsVisibleTo("DynamicDataDisplay.Markers, PublicKey=002400000480000094000000060200000024000052534131000400000100010039f88065585acdedaac491218a8836c4c54070b4b0f85bc909bd002856b509349f95fc845d1d4c664ea6b93045f2ada3b4fe70c6cd9b3fb615f94b8b5f67e4ea8ea5decb233e2e3c0ce84b78dc3ca0cd9fd2260792ece12224fca5813f03c7ad57b1faa07e3ca8fafb278fa23976fc7a35b8b4ae4efedacd1e193d89738ac2aa")] + +[assembly: AllowPartiallyTrustedCallers] + +namespace Microsoft.Research.DynamicDataDisplay +{ + public static class D3AssemblyConstants + { + public const string DefaultXmlNamespace = "http://research.microsoft.com/DynamicDataDisplay/1.0"; + } +} diff --git a/Changelog.txt b/Changelog.txt new file mode 100644 index 0000000..875e898 --- /dev/null +++ b/Changelog.txt @@ -0,0 +1 @@ +Renamed ChartPlotter's HorizontalAxis to MainHorizontalAxis, VerticalAxis to MainVerticalAxis. \ No newline at end of file diff --git a/ChartPlotter.cs b/ChartPlotter.cs new file mode 100644 index 0000000..3636632 --- /dev/null +++ b/ChartPlotter.cs @@ -0,0 +1,503 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using Microsoft.Research.DynamicDataDisplay.Charts; +using Microsoft.Research.DynamicDataDisplay.Charts.Navigation; +using Microsoft.Research.DynamicDataDisplay.Navigation; +using Microsoft.Research.DynamicDataDisplay.Common; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; + +namespace Microsoft.Research.DynamicDataDisplay +{ +/// Chart plotter is a plotter that renders axis and grid + public class ChartPlotter : Plotter2D + { + private GeneralAxis horizontalAxis = new HorizontalAxis(); + private GeneralAxis verticalAxis = new VerticalAxis(); + private AxisGrid axisGrid = new AxisGrid(); + + private readonly Legend legend = new Legend(); + + private NewLegend newLegend = new NewLegend(); + public NewLegend NewLegend + { + get { return newLegend; } + set { newLegend = value; } + } + + public ItemsPanelTemplate LegendPanelTemplate + { + get { return newLegend.ItemsPanel; } + set { newLegend.ItemsPanel = value; } + } + + public Style LegendStyle + { + get { return newLegend.Style; } + set { newLegend.Style = value; } + } + + /// + /// Initializes a new instance of the class. + /// + public ChartPlotter() + : base() + { + horizontalAxis.TicksChanged += OnHorizontalAxisTicksChanged; + verticalAxis.TicksChanged += OnVerticalAxisTicksChanged; + + SetIsDefaultAxis(horizontalAxis as DependencyObject, true); + SetIsDefaultAxis(verticalAxis as DependencyObject, true); + + mouseNavigation = new MouseNavigation(); + keyboardNavigation = new KeyboardNavigation(); + defaultContextMenu = new DefaultContextMenu(); + horizontalAxisNavigation = new AxisNavigation { Placement = AxisPlacement.Bottom }; + verticalAxisNavigation = new AxisNavigation { Placement = AxisPlacement.Left }; + + Children.AddMany( + horizontalAxis, + verticalAxis, + axisGrid, + mouseNavigation, + keyboardNavigation, + defaultContextMenu, + horizontalAxisNavigation, + legend, + verticalAxisNavigation, + new LongOperationsIndicator(), + newLegend + ); + +#if DEBUG + Children.Add(new DebugMenu()); +#endif + + SetAllChildrenAsDefault(); + } + + /// + /// Creates generic plotter from this ChartPlotter. + /// + /// + public GenericChartPlotter GetGenericPlotter() + { + return new GenericChartPlotter(this); + } + + /// + /// Creates generic plotter from this ChartPlotter. + /// Horizontal and Vertical types of GenericPlotter should correspond to ChartPlotter's actual main axes types. + /// + /// The type of horizontal values. + /// The type of vertical values. + /// GenericChartPlotter, associated to this ChartPlotter. + public GenericChartPlotter GetGenericPlotter() + { + return new GenericChartPlotter(this); + } + + /// + /// Creates generic plotter from this ChartPlotter. + /// + /// The type of the horizontal axis. + /// The type of the vertical axis. + /// The horizontal axis to use as data conversion source. + /// The vertical axis to use as data conversion source. + /// GenericChartPlotter, associated to this ChartPlotter + public GenericChartPlotter GetGenericPlotter(AxisBase horizontalAxis, AxisBase verticalAxis) + { + return new GenericChartPlotter(this, horizontalAxis, verticalAxis); + } + + protected ChartPlotter(PlotterLoadMode loadMode) : base(loadMode) { } + + /// + /// Creates empty plotter without any axes, navigation, etc. + /// + /// Empty plotter without any axes, navigation, etc. + public static ChartPlotter CreateEmpty() + { + return new ChartPlotter(PlotterLoadMode.OnlyViewport); + } + + public void BeginLongOperation() + { + LongOperationsIndicator.BeginLongOperation(this); + } + + public void EndLongOperation() + { + LongOperationsIndicator.EndLongOperation(this); + } + + #region Default charts + + private MouseNavigation mouseNavigation; + /// + /// Gets the default mouse navigation of ChartPlotter. + /// + /// The mouse navigation. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public MouseNavigation MouseNavigation + { + get { return mouseNavigation; } + } + + private KeyboardNavigation keyboardNavigation; + /// + /// Gets the default keyboard navigation of ChartPlotter. + /// + /// The keyboard navigation. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public KeyboardNavigation KeyboardNavigation + { + get { return keyboardNavigation; } + } + + private DefaultContextMenu defaultContextMenu; + /// + /// Gets the default context menu of ChartPlotter. + /// + /// The default context menu. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public DefaultContextMenu DefaultContextMenu + { + get { return defaultContextMenu; } + } + + private AxisNavigation horizontalAxisNavigation; + /// + /// Gets the default horizontal axis navigation of ChartPlotter. + /// + /// The horizontal axis navigation. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AxisNavigation HorizontalAxisNavigation + { + get { return horizontalAxisNavigation; } + } + + private AxisNavigation verticalAxisNavigation; + /// + /// Gets the default vertical axis navigation of ChartPlotter. + /// + /// The vertical axis navigation. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AxisNavigation VerticalAxisNavigation + { + get { return verticalAxisNavigation; } + } + + /// + /// Gets the default axis grid of ChartPlotter. + /// + /// The axis grid. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AxisGrid AxisGrid + { + get { return axisGrid; } + } + + #endregion + + private void OnHorizontalAxisTicksChanged(object sender, EventArgs e) + { + GeneralAxis axis = (GeneralAxis)sender; + UpdateHorizontalTicks(axis); + } + + private void UpdateHorizontalTicks(GeneralAxis axis) + { + axisGrid.BeginTicksUpdate(); + + if (axis != null) + { + axisGrid.HorizontalTicks = axis.ScreenTicks; + axisGrid.MinorHorizontalTicks = axis.MinorScreenTicks; + } + else + { + axisGrid.HorizontalTicks = null; + axisGrid.MinorHorizontalTicks = null; + } + + axisGrid.EndTicksUpdate(); + } + + private void OnVerticalAxisTicksChanged(object sender, EventArgs e) + { + GeneralAxis axis = (GeneralAxis)sender; + UpdateVerticalTicks(axis); + } + + private void UpdateVerticalTicks(GeneralAxis axis) + { + axisGrid.BeginTicksUpdate(); + + if (axis != null) + { + axisGrid.VerticalTicks = axis.ScreenTicks; + axisGrid.MinorVerticalTicks = axis.MinorScreenTicks; + } + else + { + axisGrid.VerticalTicks = null; + axisGrid.MinorVerticalTicks = null; + } + + axisGrid.EndTicksUpdate(); + } + + bool keepOldAxis = false; + bool updatingAxis = false; + + /// + /// Gets or sets the main vertical axis of ChartPlotter. + /// Main vertical axis of ChartPlotter is axis which ticks are used to draw horizontal lines on AxisGrid. + /// Value can be set to null to completely remove main vertical axis. + /// + /// The main vertical axis. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GeneralAxis MainVerticalAxis + { + get { return verticalAxis; } + set + { + if (updatingAxis) + return; + + if (value == null && verticalAxis != null) + { + if (!keepOldAxis) + { + Children.Remove(verticalAxis); + } + verticalAxis.TicksChanged -= OnVerticalAxisTicksChanged; + verticalAxis = null; + + UpdateVerticalTicks(verticalAxis); + + return; + } + + VerifyAxisType(value.Placement, AxisType.Vertical); + + if (value != verticalAxis) + { + ValidateVerticalAxis(value); + + updatingAxis = true; + + if (verticalAxis != null) + { + verticalAxis.TicksChanged -= OnVerticalAxisTicksChanged; + SetIsDefaultAxis(verticalAxis, false); + if (!keepOldAxis) + { + Children.Remove(verticalAxis); + } + } + SetIsDefaultAxis(value, true); + + verticalAxis = value; + verticalAxis.TicksChanged += OnVerticalAxisTicksChanged; + + if (!Children.Contains(value)) + { + Children.Add(value); + } + + UpdateVerticalTicks(value); + OnVerticalAxisChanged(); + + updatingAxis = false; + } + } + } + + protected virtual void OnVerticalAxisChanged() { } + protected virtual void ValidateVerticalAxis(GeneralAxis axis) { } + + /// + /// Gets or sets the main horizontal axis visibility. + /// + /// The main horizontal axis visibility. + public Visibility MainHorizontalAxisVisibility + { + get { return MainHorizontalAxis != null ? ((UIElement)MainHorizontalAxis).Visibility : Visibility.Hidden; } + set + { + if (MainHorizontalAxis != null) + { + ((UIElement)MainHorizontalAxis).Visibility = value; + } + } + } + + /// + /// Gets or sets the main vertical axis visibility. + /// + /// The main vertical axis visibility. + public Visibility MainVerticalAxisVisibility + { + get { return MainVerticalAxis != null ? ((UIElement)MainVerticalAxis).Visibility : Visibility.Hidden; } + set + { + if (MainVerticalAxis != null) + { + ((UIElement)MainVerticalAxis).Visibility = value; + } + } + } + + /// + /// Gets or sets the main horizontal axis of ChartPlotter. + /// Main horizontal axis of ChartPlotter is axis which ticks are used to draw vertical lines on AxisGrid. + /// Value can be set to null to completely remove main horizontal axis. + /// + /// The main horizontal axis. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GeneralAxis MainHorizontalAxis + { + get { return horizontalAxis; } + set + { + if (updatingAxis) + return; + + if (value == null && horizontalAxis != null) + { + Children.Remove(horizontalAxis); + horizontalAxis.TicksChanged -= OnHorizontalAxisTicksChanged; + horizontalAxis = null; + + UpdateHorizontalTicks(horizontalAxis); + + return; + } + + VerifyAxisType(value.Placement, AxisType.Horizontal); + + if (value != horizontalAxis) + { + ValidateHorizontalAxis(value); + + updatingAxis = true; + + if (horizontalAxis != null) + { + horizontalAxis.TicksChanged -= OnHorizontalAxisTicksChanged; + SetIsDefaultAxis(horizontalAxis, false); + if (!keepOldAxis) + { + Children.Remove(horizontalAxis); + } + } + SetIsDefaultAxis(value, true); + + horizontalAxis = value; + horizontalAxis.TicksChanged += OnHorizontalAxisTicksChanged; + + if (!Children.Contains(value)) + { + Children.Add(value); + } + + UpdateHorizontalTicks(value); + OnHorizontalAxisChanged(); + + updatingAxis = false; + } + } + } + + protected virtual void OnHorizontalAxisChanged() { } + protected virtual void ValidateHorizontalAxis(GeneralAxis axis) { } + + private static void VerifyAxisType(AxisPlacement axisPlacement, AxisType axisType) + { + bool result = false; + switch (axisPlacement) + { + case AxisPlacement.Left: + case AxisPlacement.Right: + result = axisType == AxisType.Vertical; + break; + case AxisPlacement.Top: + case AxisPlacement.Bottom: + result = axisType == AxisType.Horizontal; + break; + default: + break; + } + + if (!result) + throw new ArgumentException(Strings.Exceptions.InvalidAxisPlacement); + } + + protected override void OnIsDefaultAxisChangedCore(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + GeneralAxis axis = d as GeneralAxis; + if (axis != null) + { + bool value = (bool)e.NewValue; + bool oldKeepOldAxis = keepOldAxis; + + bool horizontal = axis.Placement == AxisPlacement.Bottom || axis.Placement == AxisPlacement.Top; + keepOldAxis = true; + + if (value && horizontal) + { + MainHorizontalAxis = axis; + } + else if (value && !horizontal) + { + MainVerticalAxis = axis; + } + else if (!value && horizontal) + { + MainHorizontalAxis = null; + } + else if (!value && !horizontal) + { + MainVerticalAxis = null; + } + + keepOldAxis = oldKeepOldAxis; + } + } + + /// + /// Gets the default legend of ChartPlotter. + /// + /// The legend. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Legend Legend + { + get { return legend; } + } + + /// + /// Gets or sets the visibility of legend. + /// + /// The legend visibility. + public Visibility LegendVisibility + { + get { return legend.Visibility; } + set { legend.Visibility = value; } + } + + public bool NewLegendVisible + { + get { return newLegend.LegendVisible; } + set { newLegend.LegendVisible = value; } + } + + private enum AxisType + { + Horizontal, + Vertical + } + } +} \ No newline at end of file diff --git a/Charts/Axes/AxisBase.cs b/Charts/Axes/AxisBase.cs new file mode 100644 index 0000000..6c87394 --- /dev/null +++ b/Charts/Axes/AxisBase.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Data; +using System.Diagnostics; +using System.ComponentModel; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; +using Microsoft.Research.DynamicDataDisplay.Common; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using System.Windows.Threading; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a base class for all axes in ChartPlotter. + /// Contains a real UI representation of axis - AxisControl, and means to adjust number of ticks, algorythms of their generating and + /// look of ticks' labels. + /// + /// Type of each tick's value + public abstract class AxisBase : GeneralAxis, ITypedAxis, IValueConversion + { + /// + /// Initializes a new instance of the class. + /// + /// The axis control. + /// The convert from double. + /// The convert to double. + protected AxisBase(AxisControl axisControl, Func convertFromDouble, Func convertToDouble) + { + if (axisControl == null) + throw new ArgumentNullException("axisControl"); + if (convertFromDouble == null) + throw new ArgumentNullException("convertFromDouble"); + if (convertToDouble == null) + throw new ArgumentNullException("convertToDouble"); + + this.convertToDouble = convertToDouble; + this.convertFromDouble = convertFromDouble; + + this.axisControl = axisControl; + axisControl.MakeDependent(); + axisControl.ConvertToDouble = convertToDouble; + axisControl.ScreenTicksChanged += axisControl_ScreenTicksChanged; + + Content = axisControl; + axisControl.SetBinding(Control.BackgroundProperty, new Binding("Background") { Source = this }); + + Focusable = false; + + Loaded += OnLoaded; + } + + public override void ForceUpdate() + { + axisControl.UpdateUI(); + } + + private void axisControl_ScreenTicksChanged(object sender, EventArgs e) + { + RaiseTicksChanged(); + } + + /// + /// Gets or sets a value indicating whether this axis is default axis. + /// ChartPlotter's AxisGrid gets axis ticks to display from two default axes - horizontal and vertical. + /// + /// + /// true if this instance is default axis; otherwise, false. + /// + public bool IsDefaultAxis + { + get { return Microsoft.Research.DynamicDataDisplay.Plotter.GetIsDefaultAxis(this); } + set { Microsoft.Research.DynamicDataDisplay.Plotter.SetIsDefaultAxis(this, value); } + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + RaiseTicksChanged(); + } + + /// + /// Gets the screen coordinates of axis ticks. + /// + /// The screen ticks. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override double[] ScreenTicks + { + get { return axisControl.ScreenTicks; } + } + + /// + /// Gets the screen coordinates of minor ticks. + /// + /// The minor screen ticks. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override MinorTickInfo[] MinorScreenTicks + { + get { return axisControl.MinorScreenTicks; } + } + + private AxisControl axisControl; + /// + /// Gets the axis control - actual UI representation of axis. + /// + /// The axis control. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AxisControl AxisControl + { + get { return axisControl; } + } + + /// + /// Gets or sets the ticks provider, which is used to generate ticks in given range. + /// + /// The ticks provider. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ITicksProvider TicksProvider + { + get { return axisControl.TicksProvider; } + set { axisControl.TicksProvider = value; } + } + + /// + /// Gets or sets the label provider, that is used to create UI look of axis ticks. + /// + /// Should not be null. + /// + /// The label provider. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [NotNull] + public LabelProviderBase LabelProvider + { + get { return axisControl.LabelProvider; } + set { axisControl.LabelProvider = value; } + } + + /// + /// Gets or sets the major label provider, which creates labels for major ticks. + /// If null, major labels will not be shown. + /// + /// The major label provider. + public LabelProviderBase MajorLabelProvider + { + get { return axisControl.MajorLabelProvider; } + set { axisControl.MajorLabelProvider = value; } + } + + /// + /// Gets or sets the label string format, used to create simple formats of each tick's label, such as + /// changing tick label from "1.2" to "$1.2". + /// Should be in format "*{0}*", where '*' is any number of any chars. + /// + /// If value is null, format string will not be used. + /// + /// The label string format. + public string LabelStringFormat + { + get { return LabelProvider.LabelStringFormat; } + set { LabelProvider.LabelStringFormat = value; } + } + + /// + /// Gets or sets a value indicating whether to show minor ticks. + /// + /// true if show minor ticks; otherwise, false. + public bool ShowMinorTicks + { + get { return axisControl.DrawMinorTicks; } + set { axisControl.DrawMinorTicks = value; } + } + + /// + /// Gets or sets a value indicating whether to show major labels. + /// + /// true if show major labels; otherwise, false. + public bool ShowMajorLabels + { + get { return axisControl.DrawMajorLabels; } + set { axisControl.DrawMajorLabels = value; } + } + + protected override void OnPlotterAttached(Plotter2D plotter) + { + plotter.Viewport.PropertyChanged += OnViewportPropertyChanged; + + Panel panel = GetPanelByPlacement(Placement); + if (panel != null) + { + int index = GetInsertionIndexByPlacement(Placement, panel); + panel.Children.Insert(index, this); + } + + using (axisControl.OpenUpdateRegion(true)) + { + UpdateAxisControl(plotter); + } + } + + private void UpdateAxisControl(Plotter2D plotter2d) + { + axisControl.Transform = plotter2d.Viewport.Transform; + axisControl.Range = CreateRangeFromRect(plotter2d.Visible.ViewportToData(plotter2d.Viewport.Transform)); + } + + private int GetInsertionIndexByPlacement(AxisPlacement placement, Panel panel) + { + int index = panel.Children.Count; + + switch (placement) + { + case AxisPlacement.Left: + index = 0; + break; + case AxisPlacement.Top: + index = 0; + break; + default: + break; + } + + return index; + } + + ExtendedPropertyChangedEventArgs visibleChangedEventArgs; + int viewportPropertyChangedEnters = 0; + DataRect prevDataRect = DataRect.Empty; + private void OnViewportPropertyChanged(object sender, ExtendedPropertyChangedEventArgs e) + { + if (viewportPropertyChangedEnters > 4) + { + if (e.PropertyName == "Visible") + { + visibleChangedEventArgs = e; + } + return; + } + + viewportPropertyChangedEnters++; + + Viewport2D viewport = (Viewport2D)sender; + + DataRect visible = viewport.Visible; + + DataRect dataRect = visible.ViewportToData(viewport.Transform); + bool forceUpdate = dataRect != prevDataRect; + prevDataRect = dataRect; + + Range range = CreateRangeFromRect(dataRect); + + using (axisControl.OpenUpdateRegion(false)) // todo was forceUpdate + { + axisControl.Range = range; + axisControl.Transform = viewport.Transform; + } + + Dispatcher.BeginInvoke(() => + { + viewportPropertyChangedEnters--; + if (visibleChangedEventArgs != null) + { + OnViewportPropertyChanged(Plotter.Viewport, visibleChangedEventArgs); + } + visibleChangedEventArgs = null; + }, DispatcherPriority.Render); + } + + private Func convertFromDouble; + /// + /// Gets or sets the delegate that is used to create each tick from double. + /// Is used to create typed range to display for internal AxisControl. + /// If changed, ConvertToDouble should be changed appropriately, too. + /// Should not be null. + /// + /// The convert from double. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [NotNull] + public Func ConvertFromDouble + { + get { return convertFromDouble; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (convertFromDouble != value) + { + convertFromDouble = value; + if (ParentPlotter != null) + { + UpdateAxisControl(ParentPlotter); + } + } + } + } + + private Func convertToDouble; + /// + /// Gets or sets the delegate that is used to convert each tick to double. + /// Is used by internal AxisControl to convert tick to double to get tick's coordinates inside of viewport. + /// If changed, ConvertFromDouble should be changed appropriately, too. + /// Should not be null. + /// + /// The convert to double. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [NotNull] + public Func ConvertToDouble + { + get { return convertToDouble; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (convertToDouble != value) + { + convertToDouble = value; + axisControl.ConvertToDouble = value; + } + } + } + + /// + /// Sets conversions of axis - functions used to convert values of axis type to and from double values of viewport. + /// Sets both ConvertToDouble and ConvertFromDouble properties. + /// + /// The minimal viewport value. + /// The value of axis type, corresponding to minimal viewport value. + /// The maximal viewport value. + /// The value of axis type, corresponding to maximal viewport value. + public virtual void SetConversion(double min, T minValue, double max, T maxValue) + { + throw new NotImplementedException(); + } + + private Range CreateRangeFromRect(DataRect visible) + { + T min, max; + + Range range; + switch (Placement) + { + case AxisPlacement.Left: + case AxisPlacement.Right: + min = ConvertFromDouble(visible.YMin); + max = ConvertFromDouble(visible.YMax); + break; + case AxisPlacement.Top: + case AxisPlacement.Bottom: + min = ConvertFromDouble(visible.XMin); + max = ConvertFromDouble(visible.XMax); + break; + default: + throw new NotSupportedException(); + } + + TrySort(ref min, ref max); + range = new Range(min, max); + return range; + } + + private static void TrySort(ref TS min, ref TS max) + { + if (min is IComparable) + { + IComparable c1 = (IComparable)min; + // if min > max + if (c1.CompareTo(max) > 0) + { + TS temp = min; + min = max; + max = temp; + } + } + } + + protected override void OnPlacementChanged(AxisPlacement oldPlacement, AxisPlacement newPlacement) + { + axisControl.Placement = Placement; + if (ParentPlotter != null) + { + Panel panel = GetPanelByPlacement(oldPlacement); + panel.Children.Remove(this); + + Panel newPanel = GetPanelByPlacement(newPlacement); + int index = GetInsertionIndexByPlacement(newPlacement, newPanel); + newPanel.Children.Insert(index, this); + } + } + + protected override void OnPlotterDetaching(Plotter2D plotter) + { + if (plotter == null) + return; + + Panel panel = GetPanelByPlacement(Placement); + if (panel != null) + { + panel.Children.Remove(this); + } + + plotter.Viewport.PropertyChanged -= OnViewportPropertyChanged; + axisControl.Transform = null; + } + } +} diff --git a/Charts/Axes/AxisControl.cs b/Charts/Axes/AxisControl.cs new file mode 100644 index 0000000..ef1ac05 --- /dev/null +++ b/Charts/Axes/AxisControl.cs @@ -0,0 +1,1231 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; +using System.Windows.Data; +using Microsoft.Research.DynamicDataDisplay.Common; +using System.Windows.Threading; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Defines a base class for axis UI representation. + /// Contains a number of properties that can be used to adjust ticks set and their look. + /// + /// + [TemplatePart(Name = "PART_AdditionalLabelsCanvas", Type = typeof(StackCanvas))] + [TemplatePart(Name = "PART_CommonLabelsCanvas", Type = typeof(StackCanvas))] + [TemplatePart(Name = "PART_TicksPath", Type = typeof(Path))] + [TemplatePart(Name = "PART_ContentsGrid", Type = typeof(Grid))] + public abstract class AxisControl : AxisControlBase + { + private const string templateKey = "axisControlTemplate"; + private const string additionalLabelTransformKey = "additionalLabelsTransform"; + + private const string PART_AdditionalLabelsCanvas = "PART_AdditionalLabelsCanvas"; + private const string PART_CommonLabelsCanvas = "PART_CommonLabelsCanvas"; + private const string PART_TicksPath = "PART_TicksPath"; + private const string PART_ContentsGrid = "PART_ContentsGrid"; + + /// + /// Initializes a new instance of the class. + /// + protected AxisControl() + { + HorizontalContentAlignment = HorizontalAlignment.Stretch; + VerticalContentAlignment = VerticalAlignment.Stretch; + + Background = Brushes.Transparent; + //ClipToBounds = true; + Focusable = false; + + UpdateUIResources(); + UpdateSizeGetters(); + } + + internal void MakeDependent() + { + independent = false; + } + + /// + /// This conversion is performed to make horizontal one-string and two-string labels + /// stay at one height. + /// + /// + /// + private static AxisPlacement GetBetterPlacement(AxisPlacement placement) + { + switch (placement) + { + case AxisPlacement.Left: + return AxisPlacement.Left; + case AxisPlacement.Right: + return AxisPlacement.Right; + case AxisPlacement.Top: + return AxisPlacement.Top; + case AxisPlacement.Bottom: + return AxisPlacement.Bottom; + default: + throw new NotSupportedException(); + } + } + + #region Properties + + private AxisPlacement placement = AxisPlacement.Bottom; + /// + /// Gets or sets the placement of axis control. + /// Relative positioning of parts of axis depends on this value. + /// + /// The placement. + public AxisPlacement Placement + { + get { return placement; } + set + { + if (placement != value) + { + placement = value; + UpdateUIResources(); + UpdateSizeGetters(); + } + } + } + + private void UpdateSizeGetters() + { + switch (placement) + { + case AxisPlacement.Left: + case AxisPlacement.Right: + getSize = size => size.Height; + getCoordinate = p => p.Y; + createScreenPoint1 = d => new Point(scrCoord1, d); + createScreenPoint2 = (d, size) => new Point(scrCoord2 * size, d); + break; + case AxisPlacement.Top: + case AxisPlacement.Bottom: + getSize = size => size.Width; + getCoordinate = p => p.X; + createScreenPoint1 = d => new Point(d, scrCoord1); + createScreenPoint2 = (d, size) => new Point(d, scrCoord2 * size); + break; + default: + break; + } + + switch (placement) + { + case AxisPlacement.Left: + createDataPoint = d => new Point(0, d); + break; + case AxisPlacement.Right: + createDataPoint = d => new Point(1, d); + break; + case AxisPlacement.Top: + createDataPoint = d => new Point(d, 1); + break; + case AxisPlacement.Bottom: + createDataPoint = d => new Point(d, 0); + break; + default: + break; + } + } + + private void UpdateUIResources() + { + ResourceDictionary resources = new ResourceDictionary + { + Source = new Uri("/DynamicDataDisplay;component/Charts/Axes/AxisControlStyle.xaml", UriKind.Relative) + }; + + AxisPlacement placement = GetBetterPlacement(this.placement); + ControlTemplate template = (ControlTemplate)resources[templateKey + placement.ToString()]; + Verify.AssertNotNull(template); + var content = (FrameworkElement)template.LoadContent(); + + if (ticksPath != null && ticksPath.Data != null) + { + GeometryGroup group = (GeometryGroup)ticksPath.Data; + foreach (var child in group.Children) + { + LineGeometry geometry = (LineGeometry)child; + lineGeomPool.Put(geometry); + } + group.Children.Clear(); + } + + ticksPath = (Path)content.FindName(PART_TicksPath); + ticksPath.SnapsToDevicePixels = true; + Verify.AssertNotNull(ticksPath); + + // as this method can be called not only on loading of axisControl, but when its placement changes, internal panels + // can be not empty and their contents should be released + if (commonLabelsCanvas != null && labelProvider != null) + { + foreach (UIElement child in commonLabelsCanvas.Children) + { + if (child != null) + { + labelProvider.ReleaseLabel(child); + } + } + + labels = null; + commonLabelsCanvas.Children.Clear(); + } + + commonLabelsCanvas = (StackCanvas)content.FindName(PART_CommonLabelsCanvas); + Verify.AssertNotNull(commonLabelsCanvas); + commonLabelsCanvas.Placement = placement; + + if (additionalLabelsCanvas != null && majorLabelProvider != null) + { + foreach (UIElement child in additionalLabelsCanvas.Children) + { + if (child != null) + { + majorLabelProvider.ReleaseLabel(child); + } + } + } + + additionalLabelsCanvas = (StackCanvas)content.FindName(PART_AdditionalLabelsCanvas); + Verify.AssertNotNull(additionalLabelsCanvas); + additionalLabelsCanvas.Placement = placement; + + mainGrid = (Grid)content.FindName(PART_ContentsGrid); + Verify.AssertNotNull(mainGrid); + + mainGrid.SetBinding(Control.BackgroundProperty, new Binding { Path = new PropertyPath("Background"), Source = this }); + mainGrid.SizeChanged += new SizeChangedEventHandler(mainGrid_SizeChanged); + + Content = mainGrid; + + string transformKey = additionalLabelTransformKey + placement.ToString(); + if (resources.Contains(transformKey)) + { + additionalLabelTransform = (Transform)resources[transformKey]; + } + } + + void mainGrid_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (placement.IsBottomOrTop() && e.WidthChanged || + e.HeightChanged) + { + // this is performed because if not, whole axisControl's size was measured wrongly. + InvalidateMeasure(); + UpdateUI(); + } + } + + private bool updateOnCommonChange = true; + + internal IDisposable OpenUpdateRegion(bool forceUpdate) + { + return new UpdateRegionHolder(this, forceUpdate); + } + + private sealed class UpdateRegionHolder : IDisposable + { + private Range prevRange; + private CoordinateTransform prevTransform; + private AxisControl owner; + private bool forceUpdate = false; + + public UpdateRegionHolder(AxisControl owner) : this(owner, false) { } + + public UpdateRegionHolder(AxisControl owner, bool forceUpdate) + { + this.owner = owner; + owner.updateOnCommonChange = false; + + prevTransform = owner.transform; + prevRange = owner.range; + this.forceUpdate = forceUpdate; + } + + #region IDisposable Members + + public void Dispose() + { + owner.updateOnCommonChange = true; + + bool shouldUpdate = owner.range != prevRange; + + var screenRect = owner.Transform.ScreenRect; + var prevScreenRect = prevTransform.ScreenRect; + if (owner.placement.IsBottomOrTop()) + { + shouldUpdate |= prevScreenRect.Width != screenRect.Width; + } + else + { + shouldUpdate |= prevScreenRect.Height != screenRect.Height; + } + + shouldUpdate |= owner.transform.DataTransform != prevTransform.DataTransform; + shouldUpdate |= forceUpdate; + + if (shouldUpdate) + { + owner.UpdateUI(); + } + owner = null; + } + + #endregion + } + + private Range range; + /// + /// Gets or sets the range, which ticks are generated for. + /// + /// The range. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Range Range + { + get { return range; } + set + { + range = value; + if (updateOnCommonChange) + { + UpdateUI(); + } + } + } + + private bool drawMinorTicks = true; + /// + /// Gets or sets a value indicating whether to show minor ticks. + /// + /// true if show minor ticks; otherwise, false. + public bool DrawMinorTicks + { + get { return drawMinorTicks; } + set + { + if (drawMinorTicks != value) + { + drawMinorTicks = value; + UpdateUI(); + } + } + } + + private bool drawMajorLabels = true; + /// + /// Gets or sets a value indicating whether to show major labels. + /// + /// true if show major labels; otherwise, false. + public bool DrawMajorLabels + { + get { return drawMajorLabels; } + set + { + if (drawMajorLabels != value) + { + drawMajorLabels = value; + UpdateUI(); + } + } + } + + private bool drawTicks = true; + public bool DrawTicks + { + get { return drawTicks; } + set + { + if (drawTicks != value) + { + drawTicks = value; + UpdateUI(); + } + } + } + + #region TicksProvider + + private ITicksProvider ticksProvider; + /// + /// Gets or sets the ticks provider - generator of ticks for given range. + /// + /// Should not be null. + /// + /// The ticks provider. + public ITicksProvider TicksProvider + { + get { return ticksProvider; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (ticksProvider != value) + { + DetachTicksProvider(); + + ticksProvider = value; + + AttachTicksProvider(); + + UpdateUI(); + } + } + } + + private void AttachTicksProvider() + { + if (ticksProvider != null) + { + ticksProvider.Changed += ticksProvider_Changed; + } + } + + private void ticksProvider_Changed(object sender, EventArgs e) + { + UpdateUI(); + } + + private void DetachTicksProvider() + { + if (ticksProvider != null) + { + ticksProvider.Changed -= ticksProvider_Changed; + } + } + + #endregion + + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool ShouldSerializeContent() + { + return false; + } + + protected override bool ShouldSerializeProperty(DependencyProperty dp) + { + // do not serialize template - for XAML serialization + if (dp == TemplateProperty) return false; + + return base.ShouldSerializeProperty(dp); + } + + #region MajorLabelProvider + + private LabelProviderBase majorLabelProvider; + /// + /// Gets or sets the major label provider, which creates labels for major ticks. + /// If null, major labels will not be shown. + /// + /// The major label provider. + public LabelProviderBase MajorLabelProvider + { + get { return majorLabelProvider; } + set + { + if (majorLabelProvider != value) + { + DetachMajorLabelProvider(); + + majorLabelProvider = value; + + AttachMajorLabelProvider(); + + UpdateUI(); + } + } + } + + private void AttachMajorLabelProvider() + { + if (majorLabelProvider != null) + { + majorLabelProvider.Changed += majorLabelProvider_Changed; + } + } + + private void majorLabelProvider_Changed(object sender, EventArgs e) + { + UpdateUI(); + } + + private void DetachMajorLabelProvider() + { + if (majorLabelProvider != null) + { + majorLabelProvider.Changed -= majorLabelProvider_Changed; + } + } + + #endregion + + #region LabelProvider + + private LabelProviderBase labelProvider; + /// + /// Gets or sets the label provider, which generates labels for axis ticks. + /// Should not be null. + /// + /// The label provider. + [NotNull] + public LabelProviderBase LabelProvider + { + get { return labelProvider; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (labelProvider != value) + { + DetachLabelProvider(); + + labelProvider = value; + + AttachLabelProvider(); + + UpdateUI(); + } + } + } + + private void AttachLabelProvider() + { + if (labelProvider != null) + { + labelProvider.Changed += labelProvider_Changed; + } + } + + private void labelProvider_Changed(object sender, EventArgs e) + { + UpdateUI(); + } + + private void DetachLabelProvider() + { + if (labelProvider != null) + { + labelProvider.Changed -= labelProvider_Changed; + } + } + + #endregion + + private CoordinateTransform transform = CoordinateTransform.CreateDefault(); + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public CoordinateTransform Transform + { + get { return transform; } + set + { + transform = value; + if (updateOnCommonChange) + { + UpdateUI(); + } + } + } + + #endregion + + private const double defaultSmallerSize = 1; + private const double defaultBiggerSize = 150; + protected override Size MeasureOverride(Size constraint) + { + var baseSize = base.MeasureOverride(constraint); + + mainGrid.Measure(constraint); + Size gridSize = mainGrid.DesiredSize; + Size result = gridSize; + + bool isHorizontal = placement == AxisPlacement.Bottom || placement == AxisPlacement.Top; + if (Double.IsInfinity(constraint.Width) && isHorizontal) + { + result = new Size(defaultBiggerSize, gridSize.Height != 0 ? gridSize.Height : defaultSmallerSize); + } + else if (Double.IsInfinity(constraint.Height) && !isHorizontal) + { + result = new Size(gridSize.Width != 0 ? gridSize.Width : defaultSmallerSize, defaultBiggerSize); + } + + return result; + } + + //protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + //{ + // base.OnRenderSizeChanged(sizeInfo); + + // bool isHorizontal = placement == AxisPlacement.Top || placement == AxisPlacement.Bottom; + // if (isHorizontal && sizeInfo.WidthChanged || !isHorizontal && sizeInfo.HeightChanged) + // { + // UpdateUIRepresentation(); + // } + //} + + private void InitTransform(Size newRenderSize) + { + Rect dataRect = CreateDataRect(); + + transform = transform.WithRects(dataRect, new Rect(newRenderSize)); + } + + private Rect CreateDataRect() + { + double min = convertToDouble(range.Min); + double max = convertToDouble(range.Max); + + Rect dataRect; + switch (placement) + { + case AxisPlacement.Left: + case AxisPlacement.Right: + dataRect = new Rect(new Point(min, min), new Point(max, max)); + break; + case AxisPlacement.Top: + case AxisPlacement.Bottom: + dataRect = new Rect(new Point(min, min), new Point(max, max)); + break; + default: + throw new NotSupportedException(); + } + return dataRect; + } + + /// + /// Gets the Path with ticks strokes. + /// + /// The ticks path. + public Path TicksPath + { + get { return ticksPath; } + } + + private Grid mainGrid; + private StackCanvas additionalLabelsCanvas; + private StackCanvas commonLabelsCanvas; + private Path ticksPath; + private bool rendered = false; + protected override void OnRender(DrawingContext dc) + { + base.OnRender(dc); + + if (!rendered) + { + UpdateUI(); + } + rendered = true; + } + + private bool independent = true; + + private double scrCoord1 = 0; // px + private double scrCoord2 = 10; // px + /// + /// Gets or sets the size of main axis ticks. + /// + /// The size of the tick. + public double TickSize + { + get { return scrCoord2; } + set + { + if (scrCoord2 != value) + { + scrCoord2 = value; + UpdateUI(); + } + } + } + + private GeometryGroup geomGroup = new GeometryGroup(); + internal void UpdateUI() + { + if (range.IsEmpty) + return; + + if (transform == null) return; + + if (independent) + { + InitTransform(RenderSize); + } + + bool isHorizontal = Placement == AxisPlacement.Bottom || Placement == AxisPlacement.Top; + if (transform.ScreenRect.Width == 0 && isHorizontal + || transform.ScreenRect.Height == 0 && !isHorizontal) + return; + + if (!IsMeasureValid) + { + InvalidateMeasure(); + } + + CreateTicks(); + + // removing unfinite screen ticks + var tempTicks = new List(ticks); + var tempScreenTicks = new List(ticks.Length); + var tempLabels = new List(labels); + + int i = 0; + while (i < tempTicks.Count) + { + T tick = tempTicks[i]; + double screenTick = getCoordinate(createDataPoint(convertToDouble(tick)).DataToScreen(transform)); + if (screenTick.IsFinite()) + { + tempScreenTicks.Add(screenTick); + i++; + } + else + { + tempTicks.RemoveAt(i); + tempLabels.RemoveAt(i); + } + } + + ticks = tempTicks.ToArray(); + screenTicks = tempScreenTicks.ToArray(); + labels = tempLabels.ToArray(); + + // saving generated lines into pool + for (i = 0; i < geomGroup.Children.Count; i++) + { + var geometry = (LineGeometry)geomGroup.Children[i]; + lineGeomPool.Put(geometry); + } + + geomGroup = new GeometryGroup(); + geomGroup.Children = new GeometryCollection(lineGeomPool.Count); + + if (drawTicks) + DoDrawTicks(screenTicks, geomGroup.Children); + + if (drawMinorTicks) + DoDrawMinorTicks(geomGroup.Children); + + ticksPath.Data = geomGroup; + + DoDrawCommonLabels(screenTicks); + + if (drawMajorLabels) + DoDrawMajorLabels(); + + ScreenTicksChanged.Raise(this); + } + + bool drawTicksOnEmptyLabel = false; + /// + /// Gets or sets a value indicating whether to draw ticks on empty label. + /// + /// + /// true if draw ticks on empty label; otherwise, false. + /// + public bool DrawTicksOnEmptyLabel + { + get { return drawTicksOnEmptyLabel; } + set + { + if (drawTicksOnEmptyLabel != value) + { + drawTicksOnEmptyLabel = value; + UpdateUI(); + } + } + } + + private readonly ResourcePool lineGeomPool = new ResourcePool(); + private void DoDrawTicks(double[] screenTicksX, ICollection lines) + { + for (int i = 0; i < screenTicksX.Length; i++) + { + if (labels[i] == null && !drawTicksOnEmptyLabel) + continue; + + Point p1 = createScreenPoint1(screenTicksX[i]); + Point p2 = createScreenPoint2(screenTicksX[i], 1); + + LineGeometry line = lineGeomPool.GetOrCreate(); + + line.StartPoint = p1; + line.EndPoint = p2; + lines.Add(line); + } + } + + private double GetRangesRatio(Range nominator, Range denominator) + { + double nomMin = ConvertToDouble(nominator.Min); + double nomMax = ConvertToDouble(nominator.Max); + double denMin = ConvertToDouble(denominator.Min); + double denMax = ConvertToDouble(denominator.Max); + + return (nomMax - nomMin) / (denMax - denMin); + } + + Transform additionalLabelTransform = null; + private void DoDrawMajorLabels() + { + ITicksProvider majorTicksProvider = ticksProvider.MajorProvider; + additionalLabelsCanvas.Children.Clear(); + + if (majorTicksProvider != null && majorLabelProvider != null) + { + additionalLabelsCanvas.Visibility = Visibility.Visible; + + Size renderSize = RenderSize; + var majorTicks = majorTicksProvider.GetTicks(range, DefaultTicksProvider.DefaultTicksCount); + + double[] screenCoords = majorTicks.Ticks.Select(tick => createDataPoint(convertToDouble(tick))). + Select(p => p.DataToScreen(transform)).Select(p => getCoordinate(p)).ToArray(); + + // todo this is not the best decision - when displaying, for example, + // milliseconds, it causes to create hundreds and thousands of textBlocks. + double rangesRatio = GetRangesRatio(majorTicks.Ticks.GetPairs().ToArray()[0], range); + + object info = majorTicks.Info; + MajorLabelsInfo newInfo = new MajorLabelsInfo + { + Info = info, + MajorLabelsCount = (int)Math.Ceiling(rangesRatio) + }; + + var newMajorTicks = new TicksInfo + { + Info = newInfo, + Ticks = majorTicks.Ticks, + TickSizes = majorTicks.TickSizes + }; + + UIElement[] additionalLabels = MajorLabelProvider.CreateLabels(newMajorTicks); + + for (int i = 0; i < additionalLabels.Length; i++) + { + if (screenCoords[i].IsNaN()) + continue; + + UIElement tickLabel = additionalLabels[i]; + + tickLabel.Measure(renderSize); + + StackCanvas.SetCoordinate(tickLabel, screenCoords[i]); + StackCanvas.SetEndCoordinate(tickLabel, screenCoords[i + 1]); + + if (tickLabel is FrameworkElement) + ((FrameworkElement)tickLabel).LayoutTransform = additionalLabelTransform; + + additionalLabelsCanvas.Children.Add(tickLabel); + } + } + else + { + additionalLabelsCanvas.Visibility = Visibility.Collapsed; + } + } + + private int prevMinorTicksCount = DefaultTicksProvider.DefaultTicksCount; + private const int maxTickArrangeIterations = 12; + private void DoDrawMinorTicks(ICollection lines) + { + ITicksProvider minorTicksProvider = ticksProvider.MinorProvider; + if (minorTicksProvider != null) + { + int minorTicksCount = prevMinorTicksCount; + int prevActualTicksCount = -1; + ITicksInfo minorTicks; + TickCountChange result = TickCountChange.OK; + TickCountChange prevResult; + int iteration = 0; + do + { + Verify.IsTrue(++iteration < maxTickArrangeIterations); + + minorTicks = minorTicksProvider.GetTicks(range, minorTicksCount); + + prevActualTicksCount = minorTicks.Ticks.Length; + prevResult = result; + result = CheckMinorTicksArrangement(minorTicks); + if (prevResult == TickCountChange.Decrease && result == TickCountChange.Increase) + { + // stop tick number oscillating + result = TickCountChange.OK; + } + if (result == TickCountChange.Decrease) + { + int newMinorTicksCount = minorTicksProvider.DecreaseTickCount(minorTicksCount); + if (newMinorTicksCount == minorTicksCount) + { + result = TickCountChange.OK; + } + minorTicksCount = newMinorTicksCount; + } + else if (result == TickCountChange.Increase) + { + int newCount = minorTicksProvider.IncreaseTickCount(minorTicksCount); + if (newCount == minorTicksCount) + { + result = TickCountChange.OK; + } + minorTicksCount = newCount; + } + + } while (result != TickCountChange.OK); + prevMinorTicksCount = minorTicksCount; + + double[] sizes = minorTicks.TickSizes; + + double[] screenCoords = minorTicks.Ticks.Select( + coord => getCoordinate(createDataPoint(convertToDouble(coord)). + DataToScreen(transform))).ToArray(); + + minorScreenTicks = new MinorTickInfo[screenCoords.Length]; + for (int i = 0; i < screenCoords.Length; i++) + { + minorScreenTicks[i] = new MinorTickInfo(sizes[i], screenCoords[i]); + } + + for (int i = 0; i < screenCoords.Length; i++) + { + double screenCoord = screenCoords[i]; + + Point p1 = createScreenPoint1(screenCoord); + Point p2 = createScreenPoint2(screenCoord, sizes[i]); + + LineGeometry line = lineGeomPool.GetOrCreate(); + line.StartPoint = p1; + line.EndPoint = p2; + + lines.Add(line); + } + } + } + + private TickCountChange CheckMinorTicksArrangement(ITicksInfo minorTicks) + { + Size renderSize = RenderSize; + TickCountChange result = TickCountChange.OK; + if (minorTicks.Ticks.Length * 3 > getSize(renderSize)) + result = TickCountChange.Decrease; + else if (minorTicks.Ticks.Length * 6 < getSize(renderSize)) + result = TickCountChange.Increase; + return result; + } + + private bool isStaticAxis = false; + /// + /// Gets or sets a value indicating whether this instance is a static axis. + /// If axis is static, its labels from sides are shifted so that they are not clipped by axis bounds. + /// + /// + /// true if this instance is static axis; otherwise, false. + /// + public bool IsStaticAxis + { + get { return isStaticAxis; } + set + { + if (isStaticAxis != value) + { + isStaticAxis = value; + UpdateUI(); + } + } + } + + private double ToScreen(T value) + { + return getCoordinate(createDataPoint(convertToDouble(value)).DataToScreen(transform)); + } + + private double staticAxisMargin = 1; // px + + private void DoDrawCommonLabels(double[] screenTicksX) + { + Size renderSize = RenderSize; + + commonLabelsCanvas.Children.Clear(); + +#if DEBUG + if (labels != null) + { + foreach (FrameworkElement item in labels) + { + if (item != null) + Debug.Assert(item.Parent == null); + } + } +#endif + + double minCoordUnsorted = ToScreen(range.Min); + double maxCoordUnsorted = ToScreen(range.Max); + + double minCoord = Math.Min(minCoordUnsorted, maxCoordUnsorted); + double maxCoord = Math.Max(minCoordUnsorted, maxCoordUnsorted); + + double maxCoordDiff = (maxCoord - minCoord) / labels.Length / 2.0; + + double minCoordToAdd = minCoord - maxCoordDiff; + double maxCoordToAdd = maxCoord + maxCoordDiff; + + for (int i = 0; i < ticks.Length; i++) + { + FrameworkElement tickLabel = (FrameworkElement)labels[i]; + if (tickLabel == null) continue; + + Debug.Assert(((FrameworkElement)tickLabel).Parent == null); + + tickLabel.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity)); + + double screenX = screenTicksX[i]; + double coord = screenX; + + tickLabel.HorizontalAlignment = HorizontalAlignment.Center; + tickLabel.VerticalAlignment = VerticalAlignment.Center; + + if (isStaticAxis) + { + // getting real size of label + tickLabel.Measure(renderSize); + Size tickLabelSize = tickLabel.DesiredSize; + + if (Math.Abs(screenX - minCoord) < maxCoordDiff) + { + coord = minCoord + staticAxisMargin; + if (placement.IsBottomOrTop()) + tickLabel.HorizontalAlignment = HorizontalAlignment.Left; + else + tickLabel.VerticalAlignment = VerticalAlignment.Top; + } + else if (Math.Abs(screenX - maxCoord) < maxCoordDiff) + { + coord = maxCoord - getSize(tickLabelSize) / 2 - staticAxisMargin; + if (!placement.IsBottomOrTop()) + { + tickLabel.VerticalAlignment = VerticalAlignment.Bottom; + coord = maxCoord - staticAxisMargin; + } + } + } + + // label is out of visible area + if (coord < minCoord || coord > maxCoord) + { + continue; + } + + if (coord.IsNaN()) + continue; + + StackCanvas.SetCoordinate(tickLabel, coord); + + commonLabelsCanvas.Children.Add(tickLabel); + } + } + + private double GetCoordinateFromTick(T tick) + { + return getCoordinate(createDataPoint(convertToDouble(tick)).DataToScreen(transform)); + } + + private Func convertToDouble; + /// + /// Gets or sets the convertion of tick to double. + /// Should not be null. + /// + /// The convert to double. + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Func ConvertToDouble + { + get { return convertToDouble; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + convertToDouble = value; + UpdateUI(); + } + } + + internal event EventHandler ScreenTicksChanged; + private double[] screenTicks; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public double[] ScreenTicks + { + get { return screenTicks; } + } + + private MinorTickInfo[] minorScreenTicks; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public MinorTickInfo[] MinorScreenTicks + { + get { return minorScreenTicks; } + } + + ITicksInfo ticksInfo; + private T[] ticks; + private UIElement[] labels; + private const double increaseRatio = 3.0; + private const double decreaseRatio = 1.6; + + private Func getSize = size => size.Width; + private Func getCoordinate = p => p.X; + private Func createDataPoint = d => new Point(d, 0); + + private Func createScreenPoint1 = d => new Point(d, 0); + private Func createScreenPoint2 = (d, size) => new Point(d, size); + + private int previousTickCount = DefaultTicksProvider.DefaultTicksCount; + private void CreateTicks() + { + TickCountChange result = TickCountChange.OK; + TickCountChange prevResult; + + int prevActualTickCount = -1; + + int tickCount = previousTickCount; + int iteration = 0; + + do + { + Verify.IsTrue(++iteration < maxTickArrangeIterations); + + ticksInfo = ticksProvider.GetTicks(range, tickCount); + ticks = ticksInfo.Ticks; + + if (ticks.Length == prevActualTickCount) + { + result = TickCountChange.OK; + break; + } + + prevActualTickCount = ticks.Length; + + if (labels != null) + { + for (int i = 0; i < labels.Length; i++) + { + labelProvider.ReleaseLabel(labels[i]); + } + } + + labels = labelProvider.CreateLabels(ticksInfo); + + prevResult = result; + result = CheckLabelsArrangement(labels, ticks); + + if (prevResult == TickCountChange.Decrease && result == TickCountChange.Increase) + { + // stop tick number oscillating + result = TickCountChange.OK; + } + + if (result != TickCountChange.OK) + { + int prevTickCount = tickCount; + if (result == TickCountChange.Decrease) + tickCount = ticksProvider.DecreaseTickCount(tickCount); + else + { + tickCount = ticksProvider.IncreaseTickCount(tickCount); + //DebugVerify.Is(tickCount >= prevTickCount); + } + + // ticks provider could not create less ticks or tick number didn't change + if (tickCount == 0 || prevTickCount == tickCount) + { + tickCount = prevTickCount; + result = TickCountChange.OK; + } + } + } while (result != TickCountChange.OK); + + previousTickCount = tickCount; + } + + private TickCountChange CheckLabelsArrangement(UIElement[] labels, T[] ticks) + { + var actualLabels = labels.Select((label, i) => new { Label = label, Index = i }) + .Where(el => el.Label != null) + .Select(el => new { Label = el.Label, Tick = ticks[el.Index] }) + .ToList(); + + actualLabels.ForEach(item => item.Label.Measure(RenderSize)); + + var sizeInfos = actualLabels.Select(item => + new { X = GetCoordinateFromTick(item.Tick), Size = getSize(item.Label.DesiredSize) }) + .OrderBy(item => item.X).ToArray(); + + TickCountChange res = TickCountChange.OK; + + int increaseCount = 0; + for (int i = 0; i < sizeInfos.Length - 1; i++) + { + if ((sizeInfos[i].X + sizeInfos[i].Size * decreaseRatio) > sizeInfos[i + 1].X) + { + res = TickCountChange.Decrease; + break; + } + if ((sizeInfos[i].X + sizeInfos[i].Size * increaseRatio) < sizeInfos[i + 1].X) + { + increaseCount++; + } + } + if (increaseCount > sizeInfos.Length / 2) + res = TickCountChange.Increase; + + return res; + } + } + + [DebuggerDisplay("{X} + {Size}")] + internal sealed class SizeInfo : IComparable + { + public double Size { get; set; } + public double X { get; set; } + + + public int CompareTo(SizeInfo other) + { + return X.CompareTo(other.X); + } + } + + internal enum TickCountChange + { + Increase = -1, + OK = 0, + Decrease = 1 + } + + /// + /// Represents an auxiliary structure for storing additional info during major DateTime labels generation. + /// + public struct MajorLabelsInfo + { + public object Info { get; set; } + public int MajorLabelsCount { get; set; } + + public override string ToString() + { + return String.Format("{0}, Count={1}", Info, MajorLabelsCount); + } + } +} diff --git a/Charts/Axes/AxisControlBase.cs b/Charts/Axes/AxisControlBase.cs new file mode 100644 index 0000000..6d58ec2 --- /dev/null +++ b/Charts/Axes/AxisControlBase.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public abstract class AxisControlBase : ContentControl + { + #region Properties + + public HorizontalAlignment LabelsHorizontalAlignment + { + get { return (HorizontalAlignment)GetValue(LabelsHorizontalAlignmentProperty); } + set { SetValue(LabelsHorizontalAlignmentProperty, value); } + } + + public static readonly DependencyProperty LabelsHorizontalAlignmentProperty = DependencyProperty.Register( + "LabelsHorizontalAlignment", + typeof(HorizontalAlignment), + typeof(AxisControlBase), + new FrameworkPropertyMetadata(HorizontalAlignment.Center)); + + + public VerticalAlignment LabelsVerticalAlignment + { + get { return (VerticalAlignment)GetValue(LabelsVerticalAlignmentProperty); } + set { SetValue(LabelsVerticalAlignmentProperty, value); } + } + + public static readonly DependencyProperty LabelsVerticalAlignmentProperty = DependencyProperty.Register( + "LabelsVerticalAlignment", + typeof(VerticalAlignment), + typeof(AxisControlBase), + new FrameworkPropertyMetadata(VerticalAlignment.Center)); + + #endregion // end of Properties + + } +} diff --git a/Charts/Axes/AxisControlStyle.xaml b/Charts/Axes/AxisControlStyle.xaml new file mode 100644 index 0000000..5099441 --- /dev/null +++ b/Charts/Axes/AxisControlStyle.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Charts/Axes/AxisGrid.cs b/Charts/Axes/AxisGrid.cs new file mode 100644 index 0000000..819e040 --- /dev/null +++ b/Charts/Axes/AxisGrid.cs @@ -0,0 +1,287 @@ +using System.Windows; +using System.Windows.Media; +using Microsoft.Research.DynamicDataDisplay.Charts; +using System.Windows.Controls; +using System; +using System.Windows.Shapes; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Research.DynamicDataDisplay.Common; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + + +namespace Microsoft.Research.DynamicDataDisplay +{ + /// + /// Draws grid over viewport. Number of + /// grid lines depends on Plotter's MainHorizontalAxis and MainVerticalAxis ticks. + /// + public class AxisGrid : ContentControl, IPlotterElement + { + static AxisGrid() + { + Type thisType = typeof(AxisGrid); + Panel.ZIndexProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(-1)); + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + internal void BeginTicksUpdate() + { + } + internal void EndTicksUpdate() + { + UpdateUIRepresentation(); + } + + protected internal MinorTickInfo[] MinorHorizontalTicks { get; set; } + + protected internal MinorTickInfo[] MinorVerticalTicks { get; set; } + + protected internal double[] HorizontalTicks { get; set; } + + protected internal double[] VerticalTicks { get; set; } + + + private bool drawVerticalTicks = true; + /// + /// Gets or sets a value indicating whether to draw vertical tick lines. + /// + /// true if draw vertical ticks; otherwise, false. + public bool DrawVerticalTicks + { + get { return drawVerticalTicks; } + set + { + if (drawVerticalTicks != value) + { + drawVerticalTicks = value; + UpdateUIRepresentation(); + } + } + } + + private bool drawHorizontalTicks = true; + /// + /// Gets or sets a value indicating whether to draw horizontal tick lines. + /// + /// true if draw horizontal ticks; otherwise, false. + public bool DrawHorizontalTicks + { + get { return drawHorizontalTicks; } + set + { + if (drawHorizontalTicks != value) + { + drawHorizontalTicks = value; + UpdateUIRepresentation(); + } + } + } + + private bool drawHorizontalMinorTicks = false; + /// + /// Gets or sets a value indicating whether to draw horizontal minor ticks. + /// + /// + /// true if draw horizontal minor ticks; otherwise, false. + /// + public bool DrawHorizontalMinorTicks + { + get { return drawHorizontalMinorTicks; } + set + { + if (drawHorizontalMinorTicks != value) + { + drawHorizontalMinorTicks = value; + UpdateUIRepresentation(); + } + } + } + + private bool drawVerticalMinorTicks = false; + /// + /// Gets or sets a value indicating whether to draw vertical minor ticks. + /// + /// + /// true if draw vertical minor ticks; otherwise, false. + /// + public bool DrawVerticalMinorTicks + { + get { return drawVerticalMinorTicks; } + set + { + if (drawVerticalMinorTicks != value) + { + drawVerticalMinorTicks = value; + UpdateUIRepresentation(); + } + } + } + + private double gridBrushThickness = 1; + + private Path path = new Path(); + private Canvas canvas = new Canvas(); + /// + /// Initializes a new instance of the class. + /// + public AxisGrid() + { + IsHitTestVisible = false; + + canvas.ClipToBounds = true; + + path.Stroke = Brushes.LightGray; + path.StrokeThickness = gridBrushThickness; + + Content = canvas; + } + + private readonly ResourcePool lineGeometryPool = new ResourcePool(); + private readonly ResourcePool linePool = new ResourcePool(); + + private void UpdateUIRepresentation() + { + foreach (UIElement item in canvas.Children) + { + Line line = item as Line; + if (line != null) + { + linePool.Put(line); + } + } + + canvas.Children.Clear(); + Size size = RenderSize; + + DrawMinorHorizontalTicks(); + DrawMinorVerticalTicks(); + + GeometryGroup prevGroup = path.Data as GeometryGroup; + if (prevGroup != null) + { + foreach (LineGeometry geometry in prevGroup.Children) + { + lineGeometryPool.Put(geometry); + } + } + + GeometryGroup group = new GeometryGroup(); + if (HorizontalTicks != null && drawHorizontalTicks) + { + double minY = 0; + double maxY = size.Height; + + for (int i = 0; i < HorizontalTicks.Length; i++) + { + double screenX = HorizontalTicks[i]; + LineGeometry line = lineGeometryPool.GetOrCreate(); + line.StartPoint = new Point(screenX, minY); + line.EndPoint = new Point(screenX, maxY); + group.Children.Add(line); + } + } + + if (VerticalTicks != null && drawVerticalTicks) + { + double minX = 0; + double maxX = size.Width; + + for (int i = 0; i < VerticalTicks.Length; i++) + { + double screenY = VerticalTicks[i]; + LineGeometry line = lineGeometryPool.GetOrCreate(); + line.StartPoint = new Point(minX, screenY); + line.EndPoint = new Point(maxX, screenY); + group.Children.Add(line); + } + } + + canvas.Children.Add(path); + path.Data = group; + } + + private void DrawMinorVerticalTicks() + { + Size size = RenderSize; + if (MinorVerticalTicks != null && drawVerticalMinorTicks) + { + double minX = 0; + double maxX = size.Width; + + for (int i = 0; i < MinorVerticalTicks.Length; i++) + { + double screenY = MinorVerticalTicks[i].Tick; + if (screenY < 0) + continue; + if (screenY > size.Height) + continue; + + Line line = linePool.GetOrCreate(); + + line.Y1 = screenY; + line.Y2 = screenY; + line.X1 = minX; + line.X2 = maxX; + line.Stroke = Brushes.LightGray; + line.StrokeThickness = MinorVerticalTicks[i].Value * gridBrushThickness; + + canvas.Children.Add(line); + } + } + } + + private void DrawMinorHorizontalTicks() + { + Size size = RenderSize; + if (MinorHorizontalTicks != null && drawHorizontalMinorTicks) + { + double minY = 0; + double maxY = size.Height; + + for (int i = 0; i < MinorHorizontalTicks.Length; i++) + { + double screenX = MinorHorizontalTicks[i].Tick; + if (screenX < 0) + continue; + if (screenX > size.Width) + continue; + + Line line = linePool.GetOrCreate(); + line.X1 = screenX; + line.X2 = screenX; + line.Y1 = minY; + line.Y2 = maxY; + line.Stroke = Brushes.LightGray; + line.StrokeThickness = MinorHorizontalTicks[i].Value * gridBrushThickness; + + canvas.Children.Add(line); + } + } + } + + #region IPlotterElement Members + + void IPlotterElement.OnPlotterAttached(Plotter plotter) + { + this.plotter = plotter; + plotter.CentralGrid.Children.Add(this); + } + + void IPlotterElement.OnPlotterDetaching(Plotter plotter) + { + plotter.CentralGrid.Children.Remove(this); + this.plotter = null; + } + + private Plotter plotter; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Plotter Plotter + { + get { return plotter; } + } + + #endregion + } +} \ No newline at end of file diff --git a/Charts/Axes/AxisPlacement.cs b/Charts/Axes/AxisPlacement.cs new file mode 100644 index 0000000..5c6ffd2 --- /dev/null +++ b/Charts/Axes/AxisPlacement.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Defines the position of axis inside ChartPlotter. + /// + public enum AxisPlacement + { + /// + /// Axis is placed to the left. + /// + Left, + /// + /// Axis is placed to the right. + /// + Right, + /// + /// Axis is placed to the top. + /// + Top, + /// + /// Axis is placed to the bottom. + /// + Bottom + } +} diff --git a/Charts/Axes/DateTime/DateTimeAxis.cs b/Charts/Axes/DateTime/DateTimeAxis.cs new file mode 100644 index 0000000..2d71b19 --- /dev/null +++ b/Charts/Axes/DateTime/DateTimeAxis.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.ViewportRestrictions; +using System.Windows.Media; +using System.Windows; +using System.Windows.Data; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents an axis with ticks of type. + /// + public class DateTimeAxis : AxisBase + { + /// + /// Initializes a new instance of the class. + /// + public DateTimeAxis() + : base(new DateTimeAxisControl(), DoubleToDate, + dt => dt.Ticks / 10000000000.0) + { + AxisControl.SetBinding(MajorLabelBackgroundBrushProperty, new Binding("MajorLabelBackgroundBrush") { Source = this }); + AxisControl.SetBinding(MajorLabelRectangleBorderPropertyProperty, new Binding("MajorLabelRectangleBorderProperty") { Source = this }); + } + + #region VisualProperties + + /// + /// Gets or sets the major tick labels' background brush. This is a DependencyProperty. + /// + /// The major label background brush. + public Brush MajorLabelBackgroundBrush + { + get { return (Brush)GetValue(MajorLabelBackgroundBrushProperty); } + set { SetValue(MajorLabelBackgroundBrushProperty, value); } + } + + public static readonly DependencyProperty MajorLabelBackgroundBrushProperty = DependencyProperty.Register( + "MajorLabelBackgroundBrush", + typeof(Brush), + typeof(DateTimeAxis), + new FrameworkPropertyMetadata(Brushes.Beige)); + + + public Brush MajorLabelRectangleBorderProperty + { + get { return (Brush)GetValue(MajorLabelRectangleBorderPropertyProperty); } + set { SetValue(MajorLabelRectangleBorderPropertyProperty, value); } + } + + public static readonly DependencyProperty MajorLabelRectangleBorderPropertyProperty = DependencyProperty.Register( + "MajorLabelRectangleBorderProperty", + typeof(Brush), + typeof(DateTimeAxis), + new FrameworkPropertyMetadata(Brushes.Peru)); + + #endregion // end of VisualProperties + + private ViewportRestriction restriction = new DateTimeHorizontalAxisRestriction(); + protected ViewportRestriction Restriction + { + get { return restriction; } + set { restriction = value; } + } + + protected override void OnPlotterAttached(Plotter2D plotter) + { + base.OnPlotterAttached(plotter); + + plotter.Viewport.Restrictions.Add(restriction); + } + + protected override void OnPlotterDetaching(Plotter2D plotter) + { + plotter.Viewport.Restrictions.Remove(restriction); + + base.OnPlotterDetaching(plotter); + } + + private static readonly long minTicks = DateTime.MinValue.Ticks; + private static readonly long maxTicks = DateTime.MaxValue.Ticks; + private static DateTime DoubleToDate(double d) + { + long ticks = (long)(d * 10000000000L); + + // todo should we throw an exception if number of ticks is too big or small? + if (ticks < minTicks) + ticks = minTicks; + else if (ticks > maxTicks) + ticks = maxTicks; + + return new DateTime(ticks); + } + + /// + /// Sets conversions of axis - functions used to convert values of axis type to and from double values of viewport. + /// Sets both ConvertToDouble and ConvertFromDouble properties. + /// + /// The minimal viewport value. + /// The value of axis type, corresponding to minimal viewport value. + /// The maximal viewport value. + /// The value of axis type, corresponding to maximal viewport value. + public override void SetConversion(double min, DateTime minValue, double max, DateTime maxValue) + { + var conversion = new DateTimeToDoubleConversion(min, minValue, max, maxValue); + + ConvertToDouble = conversion.ToDouble; + ConvertFromDouble = conversion.FromDouble; + } + } +} diff --git a/Charts/Axes/DateTime/DateTimeAxisControl.cs b/Charts/Axes/DateTime/DateTimeAxisControl.cs new file mode 100644 index 0000000..c180cee --- /dev/null +++ b/Charts/Axes/DateTime/DateTimeAxisControl.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// AxisControl for DateTime axes. + /// + public class DateTimeAxisControl : AxisControl + { + /// + /// Initializes a new instance of the class. + /// + public DateTimeAxisControl() + { + LabelProvider = new DateTimeLabelProvider(); + TicksProvider = new DateTimeTicksProvider(); + MajorLabelProvider = new MajorDateTimeLabelProvider(); + + ConvertToDouble = dt => dt.Ticks; + + Range = new Range(DateTime.Now, DateTime.Now.AddYears(1)); + } + } +} diff --git a/Charts/Axes/DateTime/DateTimeLabelProvider.cs b/Charts/Axes/DateTime/DateTimeLabelProvider.cs new file mode 100644 index 0000000..21ab784 --- /dev/null +++ b/Charts/Axes/DateTime/DateTimeLabelProvider.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a label provider for ticks. + /// + public class DateTimeLabelProvider : DateTimeLabelProviderBase + { + /// + /// Initializes a new instance of the class. + /// + public DateTimeLabelProvider() { } + + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + object info = ticksInfo.Info; + var ticks = ticksInfo.Ticks; + + if (info is DifferenceIn) + { + DifferenceIn diff = (DifferenceIn)info; + DateFormat = GetDateFormat(diff); + } + + LabelTickInfo tickInfo = new LabelTickInfo { Info = info }; + + UIElement[] res = new UIElement[ticks.Length]; + for (int i = 0; i < ticks.Length; i++) + { + tickInfo.Tick = ticks[i]; + + string tickText = GetString(tickInfo); + UIElement label = new TextBlock { Text = tickText, ToolTip = ticks[i] }; + ApplyCustomView(tickInfo, label); + res[i] = label; + } + + return res; + } + } +} diff --git a/Charts/Axes/DateTime/DateTimeLabelProviderBase.cs b/Charts/Axes/DateTime/DateTimeLabelProviderBase.cs new file mode 100644 index 0000000..37f3c86 --- /dev/null +++ b/Charts/Axes/DateTime/DateTimeLabelProviderBase.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; +using System.Globalization; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public abstract class DateTimeLabelProviderBase : LabelProviderBase + { + private string dateFormat; + protected string DateFormat + { + get { return dateFormat; } + set { dateFormat = value; } + } + + protected override string GetStringCore(LabelTickInfo tickInfo) + { + return tickInfo.Tick.ToString(dateFormat); + } + + protected virtual string GetDateFormat(DifferenceIn diff) + { + string format = null; + + switch (diff) + { + case DifferenceIn.Year: + format = "yyyy"; + break; + case DifferenceIn.Month: + format = "MMM"; + break; + case DifferenceIn.Day: + format = "%d"; + break; + case DifferenceIn.Hour: + format = "HH:mm"; + break; + case DifferenceIn.Minute: + format = "%m"; + break; + case DifferenceIn.Second: + format = "ss"; + break; + case DifferenceIn.Millisecond: + format = "fff"; + break; + default: + break; + } + + return format; + } + } +} diff --git a/Charts/Axes/DateTime/DateTimeTicksProvider.cs b/Charts/Axes/DateTime/DateTimeTicksProvider.cs new file mode 100644 index 0000000..fb9e88a --- /dev/null +++ b/Charts/Axes/DateTime/DateTimeTicksProvider.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a ticks provider for ticks of type. + /// + public class DateTimeTicksProvider : TimeTicksProviderBase + { + /// + /// Initializes a new instance of the class. + /// + public DateTimeTicksProvider() { } + + static DateTimeTicksProvider() + { + Providers.Add(DifferenceIn.Year, new YearDateTimeProvider()); + Providers.Add(DifferenceIn.Month, new MonthDateTimeProvider()); + Providers.Add(DifferenceIn.Day, new DayDateTimeProvider()); + Providers.Add(DifferenceIn.Hour, new HourDateTimeProvider()); + Providers.Add(DifferenceIn.Minute, new MinuteDateTimeProvider()); + Providers.Add(DifferenceIn.Second, new SecondDateTimeProvider()); + Providers.Add(DifferenceIn.Millisecond, new MillisecondDateTimeProvider()); + + MinorProviders.Add(DifferenceIn.Year, new MinorDateTimeProvider(new YearDateTimeProvider())); + MinorProviders.Add(DifferenceIn.Month, new MinorDateTimeProvider(new MonthDateTimeProvider())); + MinorProviders.Add(DifferenceIn.Day, new MinorDateTimeProvider(new DayDateTimeProvider())); + MinorProviders.Add(DifferenceIn.Hour, new MinorDateTimeProvider(new HourDateTimeProvider())); + MinorProviders.Add(DifferenceIn.Minute, new MinorDateTimeProvider(new MinuteDateTimeProvider())); + MinorProviders.Add(DifferenceIn.Second, new MinorDateTimeProvider(new SecondDateTimeProvider())); + MinorProviders.Add(DifferenceIn.Millisecond, new MinorDateTimeProvider(new MillisecondDateTimeProvider())); + } + + protected sealed override TimeSpan GetDifference(DateTime start, DateTime end) + { + return end - start; + } + } + + internal static class DateTimeArrayExtensions + { + internal static int GetIndex(this DateTime[] array, DateTime value) + { + for (int i = 0; i < array.Length - 1; i++) + { + if (array[i] <= value && value < array[i + 1]) + return i; + } + + return array.Length - 1; + } + } + + internal sealed class MinorDateTimeProvider : MinorTimeProviderBase + { + public MinorDateTimeProvider(ITicksProvider owner) : base(owner) { } + + protected override bool IsInside(DateTime value, Range range) + { + return range.Min < value && value < range.Max; + } + } + + internal sealed class YearDateTimeProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Year; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 20, 10, 5, 4, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return dt.Year; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + int year = start.Year; + int newYear = (year / step) * step; + if (newYear == 0) newYear = 1; + + return new DateTime(newYear, 1, 1); + } + + protected override bool IsMinDate(DateTime dt) + { + return dt.Year == DateTime.MinValue.Year; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + if (dt.Year + step > DateTime.MaxValue.Year) + return DateTime.MaxValue; + + return dt.AddYears(step); + } + } + + internal sealed class MonthDateTimeProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Month; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 12, 6, 4, 3, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return dt.Month + (dt.Year - start.Year) * 12; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return new DateTime(start.Year, 1, 1); + } + + protected override bool IsMinDate(DateTime dt) + { + return dt.Month == DateTime.MinValue.Month; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddMonths(step); + } + } + + internal sealed class DayDateTimeProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Day; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 30, 15, 10, 5, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (dt - start).Days; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date; + } + + protected override bool IsMinDate(DateTime dt) + { + return dt.Day == 1; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddDays(step); + } + } + + internal sealed class HourDateTimeProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Hour; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 24, 12, 6, 4, 3, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (int)(dt - start).TotalHours; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date; + } + + protected override bool IsMinDate(DateTime dt) + { + return false; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddHours(step); + } + } + + internal sealed class MinuteDateTimeProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Minute; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 60, 30, 20, 15, 10, 5, 4, 3, 2 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (int)(dt - start).TotalMinutes; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date.AddHours(start.Hour); + } + + protected override bool IsMinDate(DateTime dt) + { + return false; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddMinutes(step); + } + } + + internal sealed class SecondDateTimeProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Second; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 60, 30, 20, 15, 10, 5, 4, 3, 2 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (int)(dt - start).TotalSeconds; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date.AddHours(start.Hour).AddMinutes(start.Minute); + } + + protected override bool IsMinDate(DateTime dt) + { + return false; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddSeconds(step); + } + } + + internal sealed class MillisecondDateTimeProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Millisecond; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 100, 50, 40, 25, 20, 10, 5, 4, 2 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (int)(dt - start).TotalMilliseconds; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date.AddHours(start.Hour).AddMinutes(start.Minute).AddSeconds(start.Second); + } + + protected override bool IsMinDate(DateTime dt) + { + return false; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddMilliseconds(step); + } + } +} diff --git a/Charts/Axes/DateTime/DateTimeTicksProviderBase.cs b/Charts/Axes/DateTime/DateTimeTicksProviderBase.cs new file mode 100644 index 0000000..b93564c --- /dev/null +++ b/Charts/Axes/DateTime/DateTimeTicksProviderBase.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public abstract class DateTimeTicksProviderBase : ITicksProvider + { + public event EventHandler Changed; + protected void RaiseChanged() + { + if (Changed != null) + { + Changed(this, EventArgs.Empty); + } + } + + protected static DateTime Shift(DateTime dateTime, DifferenceIn diff) + { + DateTime res = dateTime; + + switch (diff) + { + case DifferenceIn.Year: + res = res.AddYears(1); + break; + case DifferenceIn.Month: + res = res.AddMonths(1); + break; + case DifferenceIn.Day: + res = res.AddDays(1); + break; + case DifferenceIn.Hour: + res = res.AddHours(1); + break; + case DifferenceIn.Minute: + res = res.AddMinutes(1); + break; + case DifferenceIn.Second: + res = res.AddSeconds(1); + break; + case DifferenceIn.Millisecond: + res = res.AddMilliseconds(1); + break; + default: + break; + } + + return res; + } + + protected static DateTime RoundDown(DateTime dateTime, DifferenceIn diff) + { + DateTime res = dateTime; + + switch (diff) + { + case DifferenceIn.Year: + res = new DateTime(dateTime.Year, 1, 1); + break; + case DifferenceIn.Month: + res = new DateTime(dateTime.Year, dateTime.Month, 1); + break; + case DifferenceIn.Day: + res = dateTime.Date; + break; + case DifferenceIn.Hour: + res = dateTime.Date.AddHours(dateTime.Hour); + break; + case DifferenceIn.Minute: + res = dateTime.Date.AddHours(dateTime.Hour).AddMinutes(dateTime.Minute); + break; + case DifferenceIn.Second: + res = dateTime.Date.AddHours(dateTime.Hour).AddMinutes(dateTime.Minute).AddSeconds(dateTime.Second); + break; + case DifferenceIn.Millisecond: + res = dateTime.Date.AddHours(dateTime.Hour).AddMinutes(dateTime.Minute).AddSeconds(dateTime.Second).AddMilliseconds(dateTime.Millisecond); + break; + default: + break; + } + + DebugVerify.Is(res <= dateTime); + + return res; + } + + protected static DateTime RoundUp(DateTime dateTime, DifferenceIn diff) + { + DateTime res = RoundDown(dateTime, diff); + + switch (diff) + { + case DifferenceIn.Year: + res = res.AddYears(1); + break; + case DifferenceIn.Month: + res = res.AddMonths(1); + break; + case DifferenceIn.Day: + res = res.AddDays(1); + break; + case DifferenceIn.Hour: + res = res.AddHours(1); + break; + case DifferenceIn.Minute: + res = res.AddMinutes(1); + break; + case DifferenceIn.Second: + res = res.AddSeconds(1); + break; + case DifferenceIn.Millisecond: + res = res.AddMilliseconds(1); + break; + default: + break; + } + + return res; + } + + #region ITicksProvider Members + + public abstract ITicksInfo GetTicks(Range range, int ticksCount); + public abstract int DecreaseTickCount(int ticksCount); + public abstract int IncreaseTickCount(int ticksCount); + public abstract ITicksProvider MinorProvider { get; } + public abstract ITicksProvider MajorProvider { get; } + + #endregion + } +} diff --git a/Charts/Axes/DateTime/DateTimeToDoubleConversion.cs b/Charts/Axes/DateTime/DateTimeToDoubleConversion.cs new file mode 100644 index 0000000..cf9ce58 --- /dev/null +++ b/Charts/Axes/DateTime/DateTimeToDoubleConversion.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal sealed class DateTimeToDoubleConversion + { + public DateTimeToDoubleConversion(double min, DateTime minDate, double max, DateTime maxDate) + { + this.min = min; + this.length = max - min; + this.ticksMin = minDate.Ticks; + this.ticksLength = maxDate.Ticks - ticksMin; + } + + private double min; + private double length; + private long ticksMin; + private long ticksLength; + + internal DateTime FromDouble(double d) + { + double ratio = (d - min) / length; + long tick = (long)(ticksMin + ticksLength * ratio); + + tick = MathHelper.Clamp(tick, DateTime.MinValue.Ticks, DateTime.MaxValue.Ticks); + + return new DateTime(tick); + } + + internal double ToDouble(DateTime dt) + { + double ratio = (dt.Ticks - ticksMin) / (double)ticksLength; + return min + ratio * length; + } + } +} diff --git a/Charts/Axes/DateTime/DifferenceIn.cs b/Charts/Axes/DateTime/DifferenceIn.cs new file mode 100644 index 0000000..2e06414 --- /dev/null +++ b/Charts/Axes/DateTime/DifferenceIn.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public enum DifferenceIn + { + Biggest = Year, + + Year = 6, + Month = 5, + Day = 4, + Hour = 3, + Minute = 2, + Second = 1, + Millisecond = 0, + + Smallest = Millisecond + } +} diff --git a/Charts/Axes/DateTime/HorizontalDateTimeAxis.cs b/Charts/Axes/DateTime/HorizontalDateTimeAxis.cs new file mode 100644 index 0000000..98f19ae --- /dev/null +++ b/Charts/Axes/DateTime/HorizontalDateTimeAxis.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents an axis with ticks of type, which can be placed only from bottom or top of . + /// By default is placed from bottom. + /// + public class HorizontalDateTimeAxis : DateTimeAxis + { + /// + /// Initializes a new instance of the class. + /// + public HorizontalDateTimeAxis() + { + Placement = AxisPlacement.Bottom; + } + + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Left || newPlacement == AxisPlacement.Right) + throw new ArgumentException(Strings.Exceptions.HorizontalAxisCannotBeVertical); + } + } +} diff --git a/Charts/Axes/DateTime/MajorDateTimeLabelProvider.cs b/Charts/Axes/DateTime/MajorDateTimeLabelProvider.cs new file mode 100644 index 0000000..33b3eda --- /dev/null +++ b/Charts/Axes/DateTime/MajorDateTimeLabelProvider.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Diagnostics; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; +using System.Windows.Data; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a label provider for major ticks of type. + /// + public class MajorDateTimeLabelProvider : DateTimeLabelProviderBase + { + /// + /// Initializes a new instance of the class. + /// + public MajorDateTimeLabelProvider() { } + + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + object info = ticksInfo.Info; + var ticks = ticksInfo.Ticks; + UIElement[] res = new UIElement[ticks.Length - 1]; + int labelsNum = 3; + + if (info is DifferenceIn) + { + DifferenceIn diff = (DifferenceIn)info; + DateFormat = GetDateFormat(diff); + } + else if (info is MajorLabelsInfo) + { + MajorLabelsInfo mInfo = (MajorLabelsInfo)info; + DifferenceIn diff = (DifferenceIn)mInfo.Info; + DateFormat = GetDateFormat(diff); + labelsNum = mInfo.MajorLabelsCount + 1; + + //DebugVerify.Is(labelsNum < 100); + } + + DebugVerify.Is(ticks.Length < 10); + + LabelTickInfo tickInfo = new LabelTickInfo(); + for (int i = 0; i < ticks.Length - 1; i++) + { + tickInfo.Info = info; + tickInfo.Tick = ticks[i]; + + string tickText = GetString(tickInfo); + + Grid grid = new Grid { }; + + // doing binding as described at http://sdolha.spaces.live.com/blog/cns!4121802308C5AB4E!3724.entry?wa=wsignin1.0&sa=835372863 + + grid.SetBinding(Grid.BackgroundProperty, new Binding { Path = new PropertyPath("(0)", DateTimeAxis.MajorLabelBackgroundBrushProperty), RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(AxisControlBase) } }); + Rectangle rect = new Rectangle + { + StrokeThickness = 2 + }; + rect.SetBinding(Rectangle.StrokeProperty, new Binding { Path = new PropertyPath("(0)", DateTimeAxis.MajorLabelRectangleBorderPropertyProperty), RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(AxisControlBase) } }); + + Grid.SetColumn(rect, 0); + Grid.SetColumnSpan(rect, labelsNum); + + for (int j = 0; j < labelsNum; j++) + { + grid.ColumnDefinitions.Add(new ColumnDefinition()); + } + + grid.Children.Add(rect); + + for (int j = 0; j < labelsNum; j++) + { + var tb = new TextBlock + { + Text = tickText, + HorizontalAlignment = HorizontalAlignment.Center, + Margin = new Thickness(0, 3, 0, 3) + }; + Grid.SetColumn(tb, j); + grid.Children.Add(tb); + } + + ApplyCustomView(tickInfo, grid); + + res[i] = grid; + } + + return res; + } + + protected override string GetDateFormat(DifferenceIn diff) + { + string format = null; + + switch (diff) + { + case DifferenceIn.Year: + format = "yyyy"; + break; + case DifferenceIn.Month: + format = "MMMM yyyy"; + break; + case DifferenceIn.Day: + format = "%d MMMM yyyy"; + break; + case DifferenceIn.Hour: + format = "HH:mm %d MMMM yyyy"; + break; + case DifferenceIn.Minute: + format = "HH:mm %d MMMM yyyy"; + break; + case DifferenceIn.Second: + format = "HH:mm:ss %d MMMM yyyy"; + break; + case DifferenceIn.Millisecond: + format = "fff"; + break; + default: + break; + } + + return format; + } + } +} diff --git a/Charts/Axes/DateTime/MinorTimeProviderBase.cs b/Charts/Axes/DateTime/MinorTimeProviderBase.cs new file mode 100644 index 0000000..a004f4b --- /dev/null +++ b/Charts/Axes/DateTime/MinorTimeProviderBase.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal abstract class MinorTimeProviderBase : ITicksProvider + { + public event EventHandler Changed; + protected void RaiseChanged() + { + if (Changed != null) + { + Changed(this, EventArgs.Empty); + } + } + + private readonly ITicksProvider provider; + public MinorTimeProviderBase(ITicksProvider provider) + { + this.provider = provider; + } + + private T[] majorTicks = new T[] { }; + internal void SetTicks(T[] ticks) + { + this.majorTicks = ticks; + } + + private double ticksSize = 0.5; + public ITicksInfo GetTicks(Range range, int ticksCount) + { + if (majorTicks.Length == 0) + return new TicksInfo(); + + ticksCount /= majorTicks.Length; + if (ticksCount == 0) + ticksCount = 2; + + var ticks = majorTicks.GetPairs().Select(r => Clip(provider.GetTicks(r, ticksCount), r)). + SelectMany(t => t.Ticks).ToArray(); + + var res = new TicksInfo + { + Ticks = ticks, + TickSizes = ArrayExtensions.CreateArray(ticks.Length, ticksSize) + }; + return res; + } + + private ITicksInfo Clip(ITicksInfo ticks, Range range) + { + var newTicks = new List(ticks.Ticks.Length); + var newSizes = new List(ticks.TickSizes.Length); + + for (int i = 0; i < ticks.Ticks.Length; i++) + { + T tick = ticks.Ticks[i]; + if (IsInside(tick, range)) + { + newTicks.Add(tick); + newSizes.Add(ticks.TickSizes[i]); + } + } + + return new TicksInfo + { + Ticks = newTicks.ToArray(), + TickSizes = newSizes.ToArray(), + Info = ticks.Info + }; + } + + protected abstract bool IsInside(T value, Range range); + + public int DecreaseTickCount(int ticksCount) + { + if (majorTicks.Length > 0) + ticksCount /= majorTicks.Length; + + int minorTicksCount = provider.DecreaseTickCount(ticksCount); + + if (majorTicks.Length > 0) + minorTicksCount *= majorTicks.Length; + + return minorTicksCount; + } + + public int IncreaseTickCount(int ticksCount) + { + if (majorTicks.Length > 0) + ticksCount /= majorTicks.Length; + + int minorTicksCount = provider.IncreaseTickCount(ticksCount); + + if (majorTicks.Length > 0) + minorTicksCount *= majorTicks.Length; + + return minorTicksCount; + } + + public ITicksProvider MinorProvider + { + get { return null; } + } + + public ITicksProvider MajorProvider + { + get { return null; } + } + } +} diff --git a/Charts/Axes/DateTime/Strategies/DefaultDateTimeTicksStrategy.cs b/Charts/Axes/DateTime/Strategies/DefaultDateTimeTicksStrategy.cs new file mode 100644 index 0000000..9bbc2c3 --- /dev/null +++ b/Charts/Axes/DateTime/Strategies/DefaultDateTimeTicksStrategy.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class DefaultDateTimeTicksStrategy : IDateTimeTicksStrategy + { + public virtual DifferenceIn GetDifference(TimeSpan span) + { + span = span.Duration(); + + DifferenceIn diff; + if (span.Days > 365) + diff = DifferenceIn.Year; + else if (span.Days > 30) + diff = DifferenceIn.Month; + else if (span.Days > 0) + diff = DifferenceIn.Day; + else if (span.Hours > 0) + diff = DifferenceIn.Hour; + else if (span.Minutes > 0) + diff = DifferenceIn.Minute; + else if (span.Seconds > 0) + diff = DifferenceIn.Second; + else + diff = DifferenceIn.Millisecond; + + return diff; + } + + public virtual bool TryGetLowerDiff(DifferenceIn diff, out DifferenceIn lowerDiff) + { + lowerDiff = diff; + + int code = (int)diff; + bool res = code > (int)DifferenceIn.Smallest; + if (res) + { + lowerDiff = (DifferenceIn)(code - 1); + } + return res; + } + + public virtual bool TryGetBiggerDiff(DifferenceIn diff, out DifferenceIn biggerDiff) + { + biggerDiff = diff; + + int code = (int)diff; + bool res = code < (int)DifferenceIn.Biggest; + if (res) + { + biggerDiff = (DifferenceIn)(code + 1); + } + return res; + } + } +} diff --git a/Charts/Axes/DateTime/Strategies/DelegateStrategy.cs b/Charts/Axes/DateTime/Strategies/DelegateStrategy.cs new file mode 100644 index 0000000..56e8c53 --- /dev/null +++ b/Charts/Axes/DateTime/Strategies/DelegateStrategy.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.DateTime.Strategies +{ + public class DelegateDateTimeStrategy : DefaultDateTimeTicksStrategy + { + private readonly Func function; + public DelegateDateTimeStrategy(Func function) + { + if (function == null) + throw new ArgumentNullException("function"); + + this.function = function; + } + + public override DifferenceIn GetDifference(TimeSpan span) + { + DifferenceIn? customResult = function(span); + + DifferenceIn result = customResult.HasValue ? + customResult.Value : + base.GetDifference(span); + + return result; + } + } +} diff --git a/Charts/Axes/DateTime/Strategies/ExtendedDaysStrategy.cs b/Charts/Axes/DateTime/Strategies/ExtendedDaysStrategy.cs new file mode 100644 index 0000000..bdf5a46 --- /dev/null +++ b/Charts/Axes/DateTime/Strategies/ExtendedDaysStrategy.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class ExtendedDaysStrategy : IDateTimeTicksStrategy + { + private static readonly DifferenceIn[] diffs = new DifferenceIn[] { + DifferenceIn.Year, + DifferenceIn.Day, + DifferenceIn.Hour, + DifferenceIn.Minute, + DifferenceIn.Second, + DifferenceIn.Millisecond + }; + + public DifferenceIn GetDifference(TimeSpan span) + { + span = span.Duration(); + + DifferenceIn diff; + if (span.Days > 365) + diff = DifferenceIn.Year; + else if (span.Days > 0) + diff = DifferenceIn.Day; + else if (span.Hours > 0) + diff = DifferenceIn.Hour; + else if (span.Minutes > 0) + diff = DifferenceIn.Minute; + else if (span.Seconds > 0) + diff = DifferenceIn.Second; + else + diff = DifferenceIn.Millisecond; + + return diff; + } + + public bool TryGetLowerDiff(DifferenceIn diff, out DifferenceIn lowerDiff) + { + lowerDiff = diff; + + int index = Array.IndexOf(diffs, diff); + if (index == -1) + return false; + + if (index == diffs.Length - 1) + return false; + + lowerDiff = diffs[index + 1]; + return true; + } + + public bool TryGetBiggerDiff(DifferenceIn diff, out DifferenceIn biggerDiff) + { + biggerDiff = diff; + + int index = Array.IndexOf(diffs, diff); + if (index == -1 || index == 0) + return false; + + biggerDiff = diffs[index - 1]; + return true; + } + } +} diff --git a/Charts/Axes/DateTime/Strategies/IDateTimeTicksStrategy.cs b/Charts/Axes/DateTime/Strategies/IDateTimeTicksStrategy.cs new file mode 100644 index 0000000..7df39b2 --- /dev/null +++ b/Charts/Axes/DateTime/Strategies/IDateTimeTicksStrategy.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public interface IDateTimeTicksStrategy + { + DifferenceIn GetDifference(TimeSpan span); + bool TryGetLowerDiff(DifferenceIn diff, out DifferenceIn lowerDiff); + bool TryGetBiggerDiff(DifferenceIn diff, out DifferenceIn biggerDiff); + } +} diff --git a/Charts/Axes/DateTime/TimePeriodTicksProvider.cs b/Charts/Axes/DateTime/TimePeriodTicksProvider.cs new file mode 100644 index 0000000..bf1219a --- /dev/null +++ b/Charts/Axes/DateTime/TimePeriodTicksProvider.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal abstract class TimePeriodTicksProvider : ITicksProvider + { + public event EventHandler Changed; + protected void RaiseChanged() + { + if (Changed != null) + { + Changed(this, EventArgs.Empty); + } + } + + protected abstract T RoundUp(T time, DifferenceIn diff); + protected abstract T RoundDown(T time, DifferenceIn diff); + + private bool differenceInited = false; + private DifferenceIn difference; + protected DifferenceIn Difference + { + get + { + if (!differenceInited) + { + difference = GetDifferenceCore(); + differenceInited = true; + } + return difference; + } + } + protected abstract DifferenceIn GetDifferenceCore(); + + private int[] tickCounts = null; + protected int[] TickCounts + { + get + { + if (tickCounts == null) + tickCounts = GetTickCountsCore(); + return tickCounts; + } + } + protected abstract int[] GetTickCountsCore(); + + public int DecreaseTickCount(int ticksCount) + { + if (ticksCount > TickCounts[0]) return TickCounts[0]; + + for (int i = 0; i < TickCounts.Length; i++) + if (ticksCount > TickCounts[i]) + return TickCounts[i]; + + return TickCounts.Last(); + } + + public int IncreaseTickCount(int ticksCount) + { + if (ticksCount >= TickCounts[0]) return TickCounts[0]; + + for (int i = TickCounts.Length - 1; i >= 0; i--) + if (ticksCount < TickCounts[i]) + return TickCounts[i]; + + return TickCounts.Last(); + } + + protected abstract int GetSpecificValue(T start, T dt); + protected abstract T GetStart(T start, int value, int step); + protected abstract bool IsMinDate(T dt); + protected abstract T AddStep(T dt, int step); + + public ITicksInfo GetTicks(Range range, int ticksCount) + { + T start = range.Min; + T end = range.Max; + DifferenceIn diff = Difference; + start = RoundDown(start, end); + end = RoundUp(start, end); + + RoundingInfo bounds = RoundingHelper.CreateRoundedRange( + GetSpecificValue(start, start), + GetSpecificValue(start, end)); + + int delta = (int)(bounds.Max - bounds.Min); + if (delta == 0) + return new TicksInfo { Ticks = new T[] { start } }; + + int step = delta / ticksCount; + + if (step == 0) step = 1; + + T tick = GetStart(start, (int)bounds.Min, step); + bool isMinDateTime = IsMinDate(tick) && step != 1; + if (isMinDateTime) + step--; + + List ticks = new List(); + T finishTick = AddStep(range.Max, step); + while (Continue(tick, finishTick)) + { + ticks.Add(tick); + tick = AddStep(tick, step); + if (isMinDateTime) + { + isMinDateTime = false; + step++; + } + } + + ticks = Trim(ticks, range); + + TicksInfo res = new TicksInfo { Ticks = ticks.ToArray(), Info = diff }; + return res; + } + + protected abstract bool Continue(T current, T end); + + protected abstract T RoundUp(T start, T end); + + protected abstract T RoundDown(T start, T end); + + protected abstract List Trim(List ticks, Range range); + + public ITicksProvider MinorProvider + { + get { throw new NotSupportedException(); } + } + + public ITicksProvider MajorProvider + { + get { throw new NotSupportedException(); } + } + } + + internal abstract class DatePeriodTicksProvider : TimePeriodTicksProvider + { + protected sealed override bool Continue(DateTime current, DateTime end) + { + return current < end; + } + + protected sealed override List Trim(List ticks, Range range) + { + int startIndex = 0; + for (int i = 0; i < ticks.Count - 1; i++) + { + if (ticks[i] <= range.Min && range.Min <= ticks[i + 1]) + { + startIndex = i; + break; + } + } + + int endIndex = ticks.Count - 1; + for (int i = ticks.Count - 1; i >= 1; i--) + { + if (ticks[i] >= range.Max && range.Max > ticks[i - 1]) + { + endIndex = i; + break; + } + } + + List res = new List(endIndex - startIndex + 1); + for (int i = startIndex; i <= endIndex; i++) + { + res.Add(ticks[i]); + } + + return res; + } + + protected sealed override DateTime RoundUp(DateTime start, DateTime end) + { + bool isPositive = (end - start).Ticks > 0; + return isPositive ? SafelyRoundUp(end) : RoundDown(end, Difference); + } + + private DateTime SafelyRoundUp(DateTime dt) + { + if (AddStep(dt, 1) == DateTime.MaxValue) + return DateTime.MaxValue; + + return RoundUp(dt, Difference); + } + + protected sealed override DateTime RoundDown(DateTime start, DateTime end) + { + bool isPositive = (end - start).Ticks > 0; + return isPositive ? RoundDown(start, Difference) : SafelyRoundUp(start); + } + + protected sealed override DateTime RoundDown(DateTime time, DifferenceIn diff) + { + DateTime res = time; + + switch (diff) + { + case DifferenceIn.Year: + res = new DateTime(time.Year, 1, 1); + break; + case DifferenceIn.Month: + res = new DateTime(time.Year, time.Month, 1); + break; + case DifferenceIn.Day: + res = time.Date; + break; + case DifferenceIn.Hour: + res = time.Date.AddHours(time.Hour); + break; + case DifferenceIn.Minute: + res = time.Date.AddHours(time.Hour).AddMinutes(time.Minute); + break; + case DifferenceIn.Second: + res = time.Date.AddHours(time.Hour).AddMinutes(time.Minute).AddSeconds(time.Second); + break; + case DifferenceIn.Millisecond: + res = time.Date.AddHours(time.Hour).AddMinutes(time.Minute).AddSeconds(time.Second).AddMilliseconds(time.Millisecond); + break; + default: + break; + } + + DebugVerify.Is(res <= time); + + return res; + } + + protected override DateTime RoundUp(DateTime dateTime, DifferenceIn diff) + { + DateTime res = RoundDown(dateTime, diff); + + switch (diff) + { + case DifferenceIn.Year: + res = res.AddYears(1); + break; + case DifferenceIn.Month: + res = res.AddMonths(1); + break; + case DifferenceIn.Day: + res = res.AddDays(1); + break; + case DifferenceIn.Hour: + res = res.AddHours(1); + break; + case DifferenceIn.Minute: + res = res.AddMinutes(1); + break; + case DifferenceIn.Second: + res = res.AddSeconds(1); + break; + case DifferenceIn.Millisecond: + res = res.AddMilliseconds(1); + break; + default: + break; + } + + return res; + } + } +} diff --git a/Charts/Axes/DateTime/VerticalDateTimeAxis.cs b/Charts/Axes/DateTime/VerticalDateTimeAxis.cs new file mode 100644 index 0000000..ded1af8 --- /dev/null +++ b/Charts/Axes/DateTime/VerticalDateTimeAxis.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.ViewportRestrictions; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class VerticalDateTimeAxis : DateTimeAxis + { + public VerticalDateTimeAxis() + { + Placement = AxisPlacement.Left; + Restriction = new DateTimeVerticalAxisRestriction(); + } + + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Bottom || newPlacement == AxisPlacement.Top) + throw new ArgumentException(Strings.Exceptions.VerticalAxisCannotBeHorizontal); + } + } +} diff --git a/Charts/Axes/DateTimeTicksProvider.cs b/Charts/Axes/DateTimeTicksProvider.cs new file mode 100644 index 0000000..c1868ab --- /dev/null +++ b/Charts/Axes/DateTimeTicksProvider.cs @@ -0,0 +1,495 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.NewAxis +{ + public class DateTimeTicksProvider : DateTimeTicksProviderBase + { + private static readonly Dictionary> providers = + new Dictionary>(); + + static DateTimeTicksProvider() + { + providers.Add(DifferenceIn.Year, new YearProvider()); + providers.Add(DifferenceIn.Month, new MonthProvider()); + providers.Add(DifferenceIn.Day, new DayProvider()); + providers.Add(DifferenceIn.Hour, new HourProvider()); + providers.Add(DifferenceIn.Minute, new MinuteProvider()); + providers.Add(DifferenceIn.Second, new SecondProvider()); + } + + private DifferenceIn diff; + /// + /// Gets the ticks. + /// + /// The range. + /// The ticks count. + /// + public override ITicksInfo GetTicks(Range range, int ticksCount) + { + Verify.Is(ticksCount > 0); + + DateTime start = range.Min; + DateTime end = range.Max; + TimeSpan length = end - start; + + diff = GetDifference(length); + + TicksInfo res = new TicksInfo { Info = diff }; + if (providers.ContainsKey(diff)) + { + ITicksInfo result = providers[diff].GetTicks(range, ticksCount); + DateTime[] mayorTicks = result.Ticks; + + res.Ticks = mayorTicks; + + DifferenceIn lowerDiff = DifferenceIn.Year; + // todo разобраться с minor ticks + bool lowerDiffExists = TryGetLowerDiff(diff, out lowerDiff); + if (lowerDiffExists && providers.ContainsKey(lowerDiff)) + { + var minorTicks = result.Ticks.GetPairs().Select(r => ((IMinorTicksProvider)providers[lowerDiff]).CreateTicks(r)). + SelectMany(m => m).ToArray(); + + res.MinorTicks = minorTicks; + } + return res; + } + + + DateTime newStart = RoundDown(start, diff); + DateTime newEnd = RoundUp(end, diff); + + DebugVerify.Is(newStart <= start); + + List resultTicks = new List(); + DateTime dt = newStart; + do + { + resultTicks.Add(dt); + dt = Shift(dt, diff); + } while (dt <= newEnd); + + while (resultTicks.Count > ticksCount) + { + var res2 = resultTicks; + resultTicks = res2.Where((date, i) => i % 2 == 0).ToList(); + } + + res.Ticks = resultTicks.ToArray(); + + return res; + } + + /// + /// Tries the get lower diff. + /// + /// The diff. + /// The lower diff. + /// + private static bool TryGetLowerDiff(DifferenceIn diff, out DifferenceIn lowerDiff) + { + lowerDiff = diff; + + int code = (int)diff; + bool res = code > 0; + if (res) + { + lowerDiff = (DifferenceIn)(code - 1); + } + return res; + } + + /// + /// Decreases the tick count. + /// + /// The tick count. + /// + public override int DecreaseTickCount(int tickCount) + { + if (providers.ContainsKey(diff)) + return providers[diff].DecreaseTickCount(tickCount); + + int res = tickCount / 2; + if (res < 2) res = 2; + return res; + } + + /// + /// Increases the tick count. + /// + /// The tick count. + /// + public override int IncreaseTickCount(int tickCount) + { + DebugVerify.Is(tickCount < 2000); + + if (providers.ContainsKey(diff)) + return providers[diff].IncreaseTickCount(tickCount); + + return tickCount * 2; + } + } + + public enum DifferenceIn + { + Year = 7, + Month = 6, + Day = 5, + Hour = 4, + Minute = 3, + Second = 2, + Millisecond = 1 + } + + internal static class DateTimeArrayExt + { + [Obsolete("Works wrongly", true)] + internal static DateTime[] Clip(this DateTime[] array, DateTime start, DateTime end) + { + if (start > end) + { + DateTime temp = start; + start = end; + end = temp; + } + + int startIndex = array.GetIndex(start); + int endIndex = array.GetIndex(end) + 1; + DateTime[] res = new DateTime[endIndex - startIndex]; + Array.Copy(array, startIndex, res, 0, res.Length); + + return res; + } + + internal static int GetIndex(this DateTime[] array, DateTime value) + { + for (int i = 0; i < array.Length - 1; i++) + { + if (array[i] <= value && value < array[i + 1]) + return i; + } + + return array.Length - 1; + } + } + + internal abstract class DatePeriodTicksProvider : DateTimeTicksProviderBase, IMinorTicksProvider + { + protected DatePeriodTicksProvider() + { + tickCounts = GetTickCountsCore(); + difference = GetDifferenceCore(); + } + + protected DifferenceIn difference; + protected abstract DifferenceIn GetDifferenceCore(); + + protected abstract int[] GetTickCountsCore(); + protected int[] tickCounts = { }; + + public sealed override int DecreaseTickCount(int ticksCount) + { + if (ticksCount > tickCounts[0]) return tickCounts[0]; + + for (int i = 0; i < tickCounts.Length; i++) + if (ticksCount > tickCounts[i]) + return tickCounts[i]; + + return tickCounts.Last(); + } + + public sealed override int IncreaseTickCount(int ticksCount) + { + if (ticksCount >= tickCounts[0]) return tickCounts[0]; + + for (int i = tickCounts.Length - 1; i >= 0; i--) + if (ticksCount < tickCounts[i]) + return tickCounts[i]; + + return tickCounts.Last(); + } + + protected abstract int GetSpecificValue(DateTime start, DateTime dt); + protected abstract DateTime GetStart(DateTime start, int value, int step); + protected abstract bool IsMinDate(DateTime dt); + protected abstract DateTime AddStep(DateTime dt, int step); + + public sealed override ITicksInfo GetTicks(Range range, int ticksCount) + { + DateTime start = range.Min; + DateTime end = range.Max; + TimeSpan length = end - start; + + bool isPositive = length.Ticks > 0; + DifferenceIn diff = difference; + + DateTime newStart = isPositive ? RoundDown(start, diff) : SafelyRoundUp(start); + DateTime newEnd = isPositive ? SafelyRoundUp(end) : RoundDown(end, diff); + + RoundingInfo bounds = RoundHelper.CreateRoundedRange(GetSpecificValue(newStart, newStart), GetSpecificValue(newStart, newEnd)); + + int delta = (int)(bounds.Max - bounds.Min); + if (delta == 0) + return new TicksInfo { Ticks = new DateTime[] { newStart } }; + + int step = delta / ticksCount; + + if (step == 0) step = 1; + + DateTime tick = GetStart(newStart, (int)bounds.Min, step); + bool isMinDateTime = IsMinDate(tick) && step != 1; + if (isMinDateTime) + step--; + + List ticks = new List(); + DateTime finishTick = AddStep(range.Max, step); + while (tick < finishTick) + { + ticks.Add(tick); + tick = AddStep(tick, step); + if (isMinDateTime) + { + isMinDateTime = false; + step++; + } + } + + TicksInfo res = new TicksInfo { Ticks = ticks.ToArray(), Info = diff }; + return res; + } + + private DateTime SafelyRoundUp(DateTime dt) + { + if (AddStep(dt, 1) == DateTime.MaxValue) + return DateTime.MaxValue; + + return RoundUp(dt, difference); + } + + #region IMinorTicksProvider Members + + public MinorTickInfo[] CreateTicks(Range range) + { + int tickCount = tickCounts[1]; + ITicksInfo ticks = GetTicks(range, tickCount); + + MinorTickInfo[] res = ticks.Ticks. + Select(dt => new MinorTickInfo(0.5, dt)).ToArray(); + + return res; + } + + #endregion + } + + internal class YearProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Year; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 20, 10, 5, 4, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return dt.Year; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + int year = start.Year; + int newYear = (year / step) * step; + if (newYear == 0) newYear = 1; + + return new DateTime(newYear, 1, 1); + } + + protected override bool IsMinDate(DateTime dt) + { + return dt.Year == DateTime.MinValue.Year; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + if (dt.Year + step > DateTime.MaxValue.Year) + return DateTime.MaxValue; + + return dt.AddYears(step); + } + } + + internal class MonthProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Month; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 12, 6, 4, 3, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return dt.Month + (dt.Year - start.Year) * 12; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return new DateTime(start.Year, 1, 1); + } + + protected override bool IsMinDate(DateTime dt) + { + return dt.Month == DateTime.MinValue.Month; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddMonths(step); + } + } + + internal class DayProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Day; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 30, 15, 10, 5, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (dt - start).Days; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date; + } + + protected override bool IsMinDate(DateTime dt) + { + return dt.Day == 1; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddDays(step); + } + } + + internal class HourProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Hour; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 24, 12, 6, 4, 3, 2, 1 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (dt - start).Hours; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date;//.AddHours(start.Hour); + } + + protected override bool IsMinDate(DateTime dt) + { + return false; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddHours(step); + } + } + + internal class MinuteProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Minute; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 60, 30, 20, 15, 10, 5, 4, 3, 2 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (dt - start).Minutes; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date.AddHours(start.Hour); + } + + protected override bool IsMinDate(DateTime dt) + { + return false; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddMinutes(step); + } + } + + internal class SecondProvider : DatePeriodTicksProvider + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Second; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 60, 30, 20, 15, 10, 5, 4, 3, 2 }; + } + + protected override int GetSpecificValue(DateTime start, DateTime dt) + { + return (dt - start).Seconds; + } + + protected override DateTime GetStart(DateTime start, int value, int step) + { + return start.Date.AddHours(start.Hour).AddMinutes(start.Minute); + } + + protected override bool IsMinDate(DateTime dt) + { + return false; + } + + protected override DateTime AddStep(DateTime dt, int step) + { + return dt.AddSeconds(step); + } + } +} diff --git a/Charts/Axes/DateTimeTicksProviderBase.cs b/Charts/Axes/DateTimeTicksProviderBase.cs new file mode 100644 index 0000000..e65c4bf --- /dev/null +++ b/Charts/Axes/DateTimeTicksProviderBase.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.NewAxis +{ + public abstract class DateTimeTicksProviderBase : ITicksProvider + { + public abstract ITicksInfo GetTicks(Range range, int ticksCount); + + public abstract int DecreaseTickCount(int ticksCount); + + public abstract int IncreaseTickCount(int ticksCount); + + protected static DifferenceIn GetDifference(TimeSpan span) + { + // for negative time spans + span = span.Duration(); + + DifferenceIn diff; + if (span.Days > 365) + diff = DifferenceIn.Year; + else if (span.Days > 30) + diff = DifferenceIn.Month; + else if (span.Days > 0) + diff = DifferenceIn.Day; + else if (span.Hours > 0) + diff = DifferenceIn.Hour; + else if (span.Minutes > 0) + diff = DifferenceIn.Minute; + else if (span.Seconds > 0) + diff = DifferenceIn.Second; + else + diff = DifferenceIn.Millisecond; + + return diff; + } + + protected static DateTime Shift(DateTime dateTime, DifferenceIn diff) + { + DateTime res = dateTime; + + switch (diff) + { + case DifferenceIn.Year: + res = res.AddYears(1); + break; + case DifferenceIn.Month: + res = res.AddMonths(1); + break; + case DifferenceIn.Day: + res = res.AddDays(1); + break; + case DifferenceIn.Hour: + res = res.AddHours(1); + break; + case DifferenceIn.Minute: + res = res.AddMinutes(1); + break; + case DifferenceIn.Second: + res = res.AddSeconds(1); + break; + case DifferenceIn.Millisecond: + res = res.AddMilliseconds(1); + break; + default: + break; + } + + return res; + } + + protected static DateTime RoundDown(DateTime dateTime, DifferenceIn diff) + { + DateTime res = dateTime; + + switch (diff) + { + case DifferenceIn.Year: + res = new DateTime(dateTime.Year, 1, 1); + break; + case DifferenceIn.Month: + res = new DateTime(dateTime.Year, dateTime.Month, 1); + break; + case DifferenceIn.Day: + res = dateTime.Date; + break; + case DifferenceIn.Hour: + res = dateTime.Date.AddHours(dateTime.Hour); + break; + case DifferenceIn.Minute: + res = dateTime.Date.AddHours(dateTime.Hour).AddMinutes(dateTime.Minute); + break; + case DifferenceIn.Second: + res = dateTime.Date.AddHours(dateTime.Hour).AddMinutes(dateTime.Minute).AddSeconds(dateTime.Second); + break; + case DifferenceIn.Millisecond: + res = dateTime.Date.AddHours(dateTime.Hour).AddMinutes(dateTime.Minute).AddSeconds(dateTime.Second).AddMilliseconds(dateTime.Millisecond); + break; + default: + break; + } + + DebugVerify.Is(res <= dateTime); + + return res; + } + + protected static DateTime RoundUp(DateTime dateTime, DifferenceIn diff) + { + DateTime res = RoundDown(dateTime, diff); + + switch (diff) + { + case DifferenceIn.Year: + res = res.AddYears(1); + break; + case DifferenceIn.Month: + res = res.AddMonths(1); + break; + case DifferenceIn.Day: + res = res.AddDays(1); + break; + case DifferenceIn.Hour: + res = res.AddHours(1); + break; + case DifferenceIn.Minute: + res = res.AddMinutes(1); + break; + case DifferenceIn.Second: + res = res.AddSeconds(1); + break; + case DifferenceIn.Millisecond: + res = res.AddMilliseconds(1); + break; + default: + break; + } + + return res; + } + } +} diff --git a/Charts/Axes/DefaultAxisConversions.cs b/Charts/Axes/DefaultAxisConversions.cs new file mode 100644 index 0000000..d2ea6c1 --- /dev/null +++ b/Charts/Axes/DefaultAxisConversions.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Contains default axis value conversions. + /// + public static class DefaultAxisConversions + { + #region double + + private static readonly Func doubleToDouble = d => d; + public static Func DoubleToDouble + { + get { return DefaultAxisConversions.doubleToDouble; } + } + + private static readonly Func doubleFromDouble = d => d; + public static Func DoubleFromDouble + { + get { return DefaultAxisConversions.doubleFromDouble; } + } + + #endregion + + #region DateTime + + private static readonly long minDateTimeTicks = DateTime.MinValue.Ticks; + private static readonly long maxDateTimeTicks = DateTime.MaxValue.Ticks; + private static readonly Func dateTimeFromDouble = d => + { + long ticks = (long)(d * 10000000000L); + + // todo should we throw an exception if number of ticks is too big or small? + if (ticks < minDateTimeTicks) + ticks = minDateTimeTicks; + else if (ticks > maxDateTimeTicks) + ticks = maxDateTimeTicks; + + return new DateTime(ticks); + }; + public static Func DateTimeFromDouble + { + get { return dateTimeFromDouble; } + } + + private static readonly Func dateTimeToDouble = dt => dt.Ticks / 10000000000.0; + public static Func DateTimeToDouble + { + get { return DefaultAxisConversions.dateTimeToDouble; } + } + + #endregion + + #region TimeSpan + + private static readonly long minTimeSpanTicks = TimeSpan.MinValue.Ticks; + private static readonly long maxTimeSpanTicks = TimeSpan.MaxValue.Ticks; + + private static readonly Func timeSpanFromDouble = d => + { + long ticks = (long)(d * 10000000000L); + + // todo should we throw an exception if number of ticks is too big or small? + if (ticks < minTimeSpanTicks) + ticks = minTimeSpanTicks; + else if (ticks > maxTimeSpanTicks) + ticks = maxTimeSpanTicks; + + return new TimeSpan(ticks); + }; + + public static Func TimeSpanFromDouble + { + get { return DefaultAxisConversions.timeSpanFromDouble; } + } + + private static readonly Func timeSpanToDouble = timeSpan => + { + return timeSpan.Ticks / 10000000000.0; + }; + + public static Func TimeSpanToDouble + { + get { return DefaultAxisConversions.timeSpanToDouble; } + } + + #endregion + + #region integer + + private readonly static Func intFromDouble = d => (int)d; + public static Func IntFromDouble + { + get { return DefaultAxisConversions.intFromDouble; } + } + + private readonly static Func intToDouble = i => (double)i; + public static Func IntToDouble + { + get { return DefaultAxisConversions.intToDouble; } + } + + #endregion + } +} diff --git a/Charts/Axes/DefaultNumericTicksProvider.cs b/Charts/Axes/DefaultNumericTicksProvider.cs new file mode 100644 index 0000000..7f6ef06 --- /dev/null +++ b/Charts/Axes/DefaultNumericTicksProvider.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using System.Collections.ObjectModel; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.NewAxis +{ + public sealed class DefaultDoubleTicksProvider : ITicksProvider + { + public double[] GetTicks(Range range, int preferredTicksCount) + { + double start = range.Min; + double finish = range.Max; + + double delta = finish - start; + + int log = (int)Math.Round(Math.Log10(delta)); + + double newStart = Round(start, log); + double newFinish = Round(finish, log); + if (newStart == newFinish) + { + log--; + newStart = Round(start, log); + newFinish = Round(finish, log); + } + + double step = (newFinish - newStart) / preferredTicksCount; + + //double[] ticks = CreateTicks(newStart, newFinish, preferredTicksCount); + double[] ticks = CreateTicks(start, finish, step); + return ticks; + } + + protected static double[] CreateTicks(double start, double finish, double step) + { + double x = step * (Math.Floor(start / step) + 1); + List res = new List(); + while (x <= finish) + { + res.Add(x); + x += step; + } + return res.ToArray(); + } + + //private static double[] CreateTicks(double start, double finish, int tickCount) + //{ + // double[] ticks = new double[tickCount]; + // if (tickCount == 0) + // return ticks; + + // DebugVerify.Is(tickCount > 0); + + // double delta = (finish - start) / (tickCount - 1); + + // for (int i = 0; i < tickCount; i++) + // { + // ticks[i] = start + i * delta; + // } + + // return ticks; + //} + + private static double Round(double number, int rem) + { + if (rem <= 0) + { + return Math.Round(number, -rem); + } + else + { + double pow = Math.Pow(10, rem - 1); + double val = pow * Math.Round(number / Math.Pow(10, rem - 1)); + return val; + } + } + + private static ReadOnlyCollection TickCount = + new ReadOnlyCollection(new int[] { 20, 10, 5, 4, 2, 1 }); + + public const int DefaultPreferredTicksCount = 10; + + public int DecreaseTickCount(int tickCount) + { + return TickCount.FirstOrDefault(tick => tick < tickCount); + } + + public int IncreaseTickCount(int tickCount) { + int newTickCount = TickCount.Reverse().FirstOrDefault(tick => tick > tickCount); + if (newTickCount == 0) + newTickCount = TickCount[0]; + + return newTickCount; + } + } +} diff --git a/Charts/Axes/DefaultTicksProvider.cs b/Charts/Axes/DefaultTicksProvider.cs new file mode 100644 index 0000000..9955c97 --- /dev/null +++ b/Charts/Axes/DefaultTicksProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal static class DefaultTicksProvider + { + internal static readonly int DefaultTicksCount = 10; + + internal static ITicksInfo GetTicks(this ITicksProvider provider, Range range) + { + return provider.GetTicks(range, DefaultTicksCount); + } + } +} diff --git a/Charts/Axes/GeneralAxis.cs b/Charts/Axes/GeneralAxis.cs new file mode 100644 index 0000000..09261dd --- /dev/null +++ b/Charts/Axes/GeneralAxis.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.ComponentModel; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + /// + /// Represents a base class for all DynamicDataDisplay's axes. + /// Has several axis-specific and all WPF-specific properties. + /// + public abstract class GeneralAxis : ContentControl, IPlotterElement + { + /// + /// Initializes a new instance of the class. + /// + protected GeneralAxis() { } + + #region Placement property + + private AxisPlacement placement = AxisPlacement.Bottom; + /// + /// Gets or sets the placement of axis - place in ChartPlotter where it should be placed. + /// + /// The placement. + public AxisPlacement Placement + { + get { return placement; } + set + { + if (placement != value) + { + ValidatePlacement(value); + AxisPlacement oldPlacement = placement; + placement = value; + OnPlacementChanged(oldPlacement, placement); + } + } + } + + protected virtual void OnPlacementChanged(AxisPlacement oldPlacement, AxisPlacement newPlacement) { } + + protected Panel GetPanelByPlacement(AxisPlacement placement) + { + Panel panel = null; + switch (placement) + { + case AxisPlacement.Left: + panel = ParentPlotter.LeftPanel; + break; + case AxisPlacement.Right: + panel = ParentPlotter.RightPanel; + break; + case AxisPlacement.Top: + panel = ParentPlotter.TopPanel; + break; + case AxisPlacement.Bottom: + panel = ParentPlotter.BottomPanel; + break; + default: + break; + } + return panel; + } + + /// + /// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc. + /// If proposed placement is wrong, throws an ArgumentException. + /// + /// The new placement. + protected virtual void ValidatePlacement(AxisPlacement newPlacement) { } + + #endregion + + protected void RaiseTicksChanged() + { + TicksChanged.Raise(this); + } + + public abstract void ForceUpdate(); + + /// + /// Occurs when ticks changes. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public event EventHandler TicksChanged; + + /// + /// Gets the screen coordinates of axis ticks. + /// + /// The screen ticks. + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract double[] ScreenTicks { get; } + + /// + /// Gets the screen coordinates of minor ticks. + /// + /// The minor screen ticks. + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract MinorTickInfo[] MinorScreenTicks { get; } + + #region IPlotterElement Members + + private Plotter2D plotter; + [EditorBrowsable(EditorBrowsableState.Never)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Plotter2D ParentPlotter + { + get { return plotter; } + } + + void IPlotterElement.OnPlotterAttached(Plotter plotter) + { + this.plotter = (Plotter2D)plotter; + OnPlotterAttached(this.plotter); + } + + protected virtual void OnPlotterAttached(Plotter2D plotter) { } + + void IPlotterElement.OnPlotterDetaching(Plotter plotter) + { + OnPlotterDetaching(this.plotter); + this.plotter = null; + } + + protected virtual void OnPlotterDetaching(Plotter2D plotter) { } + + public Plotter2D Plotter + { + get { return plotter; } + } + + Plotter IPlotterElement.Plotter + { + get { return plotter; } + } + + #endregion + } +} diff --git a/Charts/Axes/GenericLabelProvider.cs b/Charts/Axes/GenericLabelProvider.cs new file mode 100644 index 0000000..31ae882 --- /dev/null +++ b/Charts/Axes/GenericLabelProvider.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + /// + /// Represents default implementation of label provider for specified type. + /// + /// Axis values type. + public class GenericLabelProvider : LabelProviderBase + { + /// + /// Initializes a new instance of the class. + /// + public GenericLabelProvider() { } + + #region ILabelProvider Members + + /// + /// Creates the labels by given ticks info. + /// + /// The ticks info. + /// + /// Array of s, which are axis labels for specified axis ticks. + /// + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + var ticks = ticksInfo.Ticks; + var info = ticksInfo.Info; + + LabelTickInfo tickInfo = new LabelTickInfo(); + UIElement[] res = new UIElement[ticks.Length]; + for (int i = 0; i < res.Length; i++) + { + tickInfo.Tick = ticks[i]; + tickInfo.Info = info; + + string text = GetString(tickInfo); + + res[i] = new TextBlock + { + Text = text, + ToolTip = ticks[i].ToString() + }; + } + return res; + } + + #endregion + } +} diff --git a/Charts/Axes/GenericLocational/GenericLocationalLabelProvider.cs b/Charts/Axes/GenericLocational/GenericLocationalLabelProvider.cs new file mode 100644 index 0000000..7ecc0e6 --- /dev/null +++ b/Charts/Axes/GenericLocational/GenericLocationalLabelProvider.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.GenericLocational +{ + public class GenericLocationalLabelProvider : LabelProviderBase + { + private readonly IList collection; + private readonly Func displayMemberMapping; + + public GenericLocationalLabelProvider(IList collection, Func displayMemberMapping) + { + if (collection == null) + throw new ArgumentNullException("collection"); + if (displayMemberMapping == null) + throw new ArgumentNullException("displayMemberMapping"); + + this.collection = collection; + this.displayMemberMapping = displayMemberMapping; + } + + int startIndex; + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + var ticks = ticksInfo.Ticks; + + if (ticks.Length == 0) + return EmptyLabelsArray; + + startIndex = (int)ticksInfo.Info; + + UIElement[] result = new UIElement[ticks.Length]; + + LabelTickInfo labelInfo = new LabelTickInfo { Info = ticksInfo.Info }; + + for (int i = 0; i < result.Length; i++) + { + var tick = ticks[i]; + labelInfo.Tick = tick; + labelInfo.Index = i; + + string labelText = GetString(labelInfo); + + TextBlock label = new TextBlock { Text = labelText }; + + ApplyCustomView(labelInfo, label); + + result[i] = label; + } + + return result; + } + + protected override string GetStringCore(LabelTickInfo tickInfo) + { + return displayMemberMapping(collection[tickInfo.Index + startIndex]); + } + } +} diff --git a/Charts/Axes/GenericLocational/GenericLocationalTicksProvider.cs b/Charts/Axes/GenericLocational/GenericLocationalTicksProvider.cs new file mode 100644 index 0000000..ed02ab1 --- /dev/null +++ b/Charts/Axes/GenericLocational/GenericLocationalTicksProvider.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.DataSearch; +using System.Windows; +using System.Collections; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.GenericLocational +{ + public class GenericLocationalTicksProvider : ITicksProvider where TAxis : IComparable + { + private IList collection; + public IList Collection + { + get { return collection; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + Changed.Raise(this); + collection = value; + } + } + + private Func axisMapping; + public Func AxisMapping + { + get { return axisMapping; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + Changed.Raise(this); + axisMapping = value; + } + } + + /// + /// Initializes a new instance of the class. + /// + public GenericLocationalTicksProvider() { } + + /// + /// Initializes a new instance of the class. + /// + /// The collection of axis ticks and labels. + public GenericLocationalTicksProvider(IList collection) + { + Collection = collection; + } + + public GenericLocationalTicksProvider(IList collection, Func coordinateMapping) + { + Collection = collection; + AxisMapping = coordinateMapping; + } + + #region ITicksProvider Members + + SearchResult1d minResult = SearchResult1d.Empty; + SearchResult1d maxResult = SearchResult1d.Empty; + GenericSearcher1d searcher; + /// + /// Generates ticks for given range and preferred ticks count. + /// + /// The range. + /// The ticks count. + /// + public ITicksInfo GetTicks(Range range, int ticksCount) + { + EnsureSearcher(); + + //minResult = searcher.SearchBetween(range.Min, minResult); + //maxResult = searcher.SearchBetween(range.Max, maxResult); + + minResult = searcher.SearchFirstLess(range.Min); + maxResult = searcher.SearchGreater(range.Max); + + if (!(minResult.IsEmpty && maxResult.IsEmpty)) + { + int startIndex = !minResult.IsEmpty ? minResult.Index : 0; + int endIndex = !maxResult.IsEmpty ? maxResult.Index : collection.Count - 1; + + int count = endIndex - startIndex + 1; + + TAxis[] ticks = new TAxis[count]; + for (int i = startIndex; i <= endIndex; i++) + { + ticks[i - startIndex] = axisMapping(collection[i]); + } + + TicksInfo result = new TicksInfo + { + Info = startIndex, + TickSizes = ArrayExtensions.CreateArray(count, 1.0), + Ticks = ticks + }; + + return result; + } + else + { + return TicksInfo.Empty; + } + } + + private void EnsureSearcher() + { + if (searcher == null) + { + if (collection == null || axisMapping == null) + throw new InvalidOperationException(Strings.Exceptions.GenericLocationalProviderInvalidState); + + searcher = new GenericSearcher1d(collection, axisMapping); + } + } + + public int DecreaseTickCount(int ticksCount) + { + return collection.Count; + } + + public int IncreaseTickCount(int ticksCount) + { + return collection.Count; + } + + public ITicksProvider MinorProvider + { + get { return null; } + } + + public ITicksProvider MajorProvider + { + get { return null; } + } + + public event EventHandler Changed; + + #endregion + } +} diff --git a/Charts/Axes/ITicksProvider.cs b/Charts/Axes/ITicksProvider.cs new file mode 100644 index 0000000..1f9efca --- /dev/null +++ b/Charts/Axes/ITicksProvider.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Contains information about one minor tick - its value (relative size) and its tick. + /// + /// + [DebuggerDisplay("{Value} @ {Tick}")] + public struct MinorTickInfo + { + internal MinorTickInfo(double value, T tick) + { + this.value = value; + this.tick = tick; + } + + private readonly double value; + private readonly T tick; + + public double Value { get { return value; } } + public T Tick { get { return tick; } } + + public override string ToString() + { + return String.Format("{0} @ {1}", value, tick); + } + } + + /// + /// Contains data for all generated ticks. + /// Used by TicksLabelProvider. + /// + /// Type of axis tick. + public interface ITicksInfo + { + /// + /// Gets the array of axis ticks. + /// + /// The ticks. + T[] Ticks { get; } + /// + /// Gets the tick sizes. + /// + /// The tick sizes. + double[] TickSizes { get; } + /// + /// Gets the additional information, added to ticks info and specifying range's features. + /// + /// The info. + object Info { get; } + } + + internal class TicksInfo : ITicksInfo + { + private T[] ticks = { }; + /// + /// Gets the array of axis ticks. + /// + /// The ticks. + public T[] Ticks + { + get { return ticks; } + internal set { ticks = value; } + } + + private double[] tickSizes = { }; + /// + /// Gets the tick sizes. + /// + /// The tick sizes. + public double[] TickSizes + { + get + { + if (tickSizes.Length != ticks.Length) + tickSizes = ArrayExtensions.CreateArray(ticks.Length, 1.0); + + return tickSizes; + } + internal set { tickSizes = value; } + } + + private object info = null; + /// + /// Gets the additional information, added to ticks info and specifying range's features. + /// + /// The info. + public object Info + { + get { return info; } + internal set { info = value; } + } + + private static readonly TicksInfo empty = new TicksInfo { info = null, ticks = new T[0], tickSizes = new double[0] }; + internal static TicksInfo Empty + { + get { return empty; } + } + } + + /// + /// Base interface for ticks generator. + /// + /// + public interface ITicksProvider + { + /// + /// Generates ticks for given range and preferred ticks count. + /// + /// The range. + /// The ticks count. + /// + ITicksInfo GetTicks(Range range, int ticksCount); + /// + /// Decreases the tick count. + /// Returned value should be later passed as ticksCount parameter to GetTicks method. + /// + /// The ticks count. + /// Decreased ticks count. + int DecreaseTickCount(int ticksCount); + /// + /// Increases the tick count. + /// Returned value should be later passed as ticksCount parameter to GetTicks method. + /// + /// The ticks count. + /// Increased ticks count. + int IncreaseTickCount(int ticksCount); + + /// + /// Gets the minor ticks provider, used to generate ticks between each two adjacent ticks. + /// + /// The minor provider. If there is no minor provider available, returns null. + ITicksProvider MinorProvider { get; } + /// + /// Gets the major provider, used to generate major ticks - for example, years for common ticks as months. + /// + /// The major provider. If there is no major provider available, returns null. + ITicksProvider MajorProvider { get; } + + /// + /// Occurs when properties of ticks provider changeds. + /// Notifies axis to rebuild its view. + /// + event EventHandler Changed; + } +} diff --git a/Charts/Axes/ITypedAxis.cs b/Charts/Axes/ITypedAxis.cs new file mode 100644 index 0000000..68a69f8 --- /dev/null +++ b/Charts/Axes/ITypedAxis.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Describes axis as having ticks type. + /// Provides access to some typed properties. + /// + /// Axis tick's type. + public interface ITypedAxis + { + /// + /// Gets the ticks provider. + /// + /// The ticks provider. + ITicksProvider TicksProvider { get; } + /// + /// Gets the label provider. + /// + /// The label provider. + LabelProviderBase LabelProvider { get; } + + /// + /// Gets or sets the convertion of tick from double. + /// Should not be null. + /// + /// The convert from double. + Func ConvertFromDouble { get; set; } + /// + /// Gets or sets the convertion of tick to double. + /// Should not be null. + /// + /// The convert to double. + Func ConvertToDouble { get; set; } + } +} diff --git a/Charts/Axes/IValueConversion.cs b/Charts/Axes/IValueConversion.cs new file mode 100644 index 0000000..0e41d67 --- /dev/null +++ b/Charts/Axes/IValueConversion.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public interface IValueConversion + { + Func ConvertToDouble { get; set; } + Func ConvertFromDouble { get; set; } + } +} diff --git a/Charts/Axes/Integer/CollectionLabelProvider.cs b/Charts/Axes/Integer/CollectionLabelProvider.cs new file mode 100644 index 0000000..37e536c --- /dev/null +++ b/Charts/Axes/Integer/CollectionLabelProvider.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + public class CollectionLabelProvider : LabelProviderBase + { + private IList collection; + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] + public IList Collection + { + get { return collection; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (collection != value) + { + DetachCollection(); + + collection = value; + + AttachCollection(); + + RaiseChanged(); + } + } + } + + #region Collection changed + + private void AttachCollection() + { + INotifyCollectionChanged observableCollection = collection as INotifyCollectionChanged; + if (observableCollection != null) + { + observableCollection.CollectionChanged += OnCollectionChanged; + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + RaiseChanged(); + } + + private void DetachCollection() + { + INotifyCollectionChanged observableCollection = collection as INotifyCollectionChanged; + if (observableCollection != null) + { + observableCollection.CollectionChanged -= OnCollectionChanged; + } + } + + #endregion + + /// + /// Initializes a new instance of the class with empty labels collection. + /// + public CollectionLabelProvider() { } + + public CollectionLabelProvider(IList collection) + : this() + { + Collection = collection; + } + + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + var ticks = ticksInfo.Ticks; + + UIElement[] res = new UIElement[ticks.Length]; + + var tickInfo = new LabelTickInfo { Info = ticksInfo.Info }; + + for (int i = 0; i < res.Length; i++) + { + int tick = ticks[i]; + tickInfo.Tick = tick; + + if (0 <= tick && tick < collection.Count) + { + string text = collection[tick].ToString(); + res[i] = new TextBlock + { + Text = text, + ToolTip = text + }; + } + else + { + res[i] = null; + } + } + return res; + } + } +} diff --git a/Charts/Axes/Integer/HorizontalIntegerAxis.cs b/Charts/Axes/Integer/HorizontalIntegerAxis.cs new file mode 100644 index 0000000..b0f31f5 --- /dev/null +++ b/Charts/Axes/Integer/HorizontalIntegerAxis.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + public class HorizontalIntegerAxis : IntegerAxis + { + public HorizontalIntegerAxis() + { + Placement = AxisPlacement.Bottom; + } + + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Left || newPlacement == AxisPlacement.Right) + throw new ArgumentException(Strings.Exceptions.HorizontalAxisCannotBeVertical); + } + } +} diff --git a/Charts/Axes/Integer/IntegerAxis.cs b/Charts/Axes/Integer/IntegerAxis.cs new file mode 100644 index 0000000..1484ce0 --- /dev/null +++ b/Charts/Axes/Integer/IntegerAxis.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + public class IntegerAxis : AxisBase + { + public IntegerAxis() + : base(new IntegerAxisControl(), + d => (int)d, + i => (double)i) + { + + } + } +} diff --git a/Charts/Axes/Integer/IntegerAxisControl.cs b/Charts/Axes/Integer/IntegerAxisControl.cs new file mode 100644 index 0000000..29e3817 --- /dev/null +++ b/Charts/Axes/Integer/IntegerAxisControl.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + public class IntegerAxisControl : AxisControl + { + public IntegerAxisControl() + { + LabelProvider = new GenericLabelProvider(); + TicksProvider = new IntegerTicksProvider(); + ConvertToDouble = i => (double)i; + Range = new Range(0, 1); + } + } +} diff --git a/Charts/Axes/Integer/IntegerTicksProvider.cs b/Charts/Axes/Integer/IntegerTicksProvider.cs new file mode 100644 index 0000000..a5f586e --- /dev/null +++ b/Charts/Axes/Integer/IntegerTicksProvider.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + /// + /// Represents a ticks provider for intefer values. + /// + public class IntegerTicksProvider : ITicksProvider + { + /// + /// Initializes a new instance of the class. + /// + public IntegerTicksProvider() { } + + private int minStep = 0; + /// + /// Gets or sets the minimal step between ticks. + /// + /// The min step. + public int MinStep + { + get { return minStep; } + set + { + Verify.IsTrue(value >= 0, "value"); + if (minStep != value) + { + minStep = value; + RaiseChangedEvent(); + } + } + } + + private int maxStep = Int32.MaxValue; + /// + /// Gets or sets the maximal step between ticks. + /// + /// The max step. + public int MaxStep + { + get { return maxStep; } + set + { + if (maxStep != value) + { + if (value < 0) + throw new ArgumentOutOfRangeException("value", Strings.Exceptions.ParameterShouldBePositive); + + maxStep = value; + RaiseChangedEvent(); + } + } + } + + #region ITicksProvider Members + + /// + /// Generates ticks for given range and preferred ticks count. + /// + /// The range. + /// The ticks count. + /// + public ITicksInfo GetTicks(Range range, int ticksCount) + { + double start = range.Min; + double finish = range.Max; + + double delta = finish - start; + + int log = (int)Math.Round(Math.Log10(delta)); + + double newStart = RoundingHelper.Round(start, log); + double newFinish = RoundingHelper.Round(finish, log); + if (newStart == newFinish) + { + log--; + newStart = RoundingHelper.Round(start, log); + newFinish = RoundingHelper.Round(finish, log); + } + + // calculating step between ticks + double unroundedStep = (newFinish - newStart) / ticksCount; + int stepLog = log; + // trying to round step + int step = (int)RoundingHelper.Round(unroundedStep, stepLog); + if (step == 0) + { + stepLog--; + step = (int)RoundingHelper.Round(unroundedStep, stepLog); + if (step == 0) + { + // step will not be rounded if attempts to be rounded to zero. + step = (int)unroundedStep; + } + } + + if (step < minStep) + step = minStep; + if (step > maxStep) + step = maxStep; + + if (step <= 0) + step = 1; + + int[] ticks = CreateTicks(start, finish, step); + + TicksInfo res = new TicksInfo { Info = log, Ticks = ticks }; + + return res; + } + + private static int[] CreateTicks(double start, double finish, int step) + { + DebugVerify.Is(step != 0); + + int x = (int)(step * Math.Floor(start / (double)step)); + List res = new List(); + + checked + { + double increasedFinish = finish + step * 1.05; + while (x <= increasedFinish) + { + res.Add(x); + x += step; + } + } + return res.ToArray(); + } + + private static int[] tickCounts = new int[] { 20, 10, 5, 4, 2, 1 }; + + /// + /// Decreases the tick count. + /// Returned value should be later passed as ticksCount parameter to GetTicks method. + /// + /// The ticks count. + /// Decreased ticks count. + public int DecreaseTickCount(int ticksCount) + { + return tickCounts.FirstOrDefault(tick => tick < ticksCount); + } + + /// + /// Increases the tick count. + /// Returned value should be later passed as ticksCount parameter to GetTicks method. + /// + /// The ticks count. + /// Increased ticks count. + public int IncreaseTickCount(int ticksCount) + { + int newTickCount = tickCounts.Reverse().FirstOrDefault(tick => tick > ticksCount); + if (newTickCount == 0) + newTickCount = tickCounts[0]; + + return newTickCount; + } + + /// + /// Gets the minor ticks provider, used to generate ticks between each two adjacent ticks. + /// + /// The minor provider. + public ITicksProvider MinorProvider + { + get { return null; } + } + + /// + /// Gets the major provider, used to generate major ticks - for example, years for common ticks as months. + /// + /// The major provider. + public ITicksProvider MajorProvider + { + get { return null; } + } + + protected void RaiseChangedEvent() + { + Changed.Raise(this); + } + public event EventHandler Changed; + + #endregion + } +} diff --git a/Charts/Axes/Integer/VerticalIntegerAxis.cs b/Charts/Axes/Integer/VerticalIntegerAxis.cs new file mode 100644 index 0000000..9b1c95a --- /dev/null +++ b/Charts/Axes/Integer/VerticalIntegerAxis.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + public class VerticalIntegerAxis : IntegerAxis + { + public VerticalIntegerAxis() + { + Placement = AxisPlacement.Left; + } + + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Bottom || newPlacement == AxisPlacement.Top) + throw new ArgumentException(Strings.Exceptions.VerticalAxisCannotBeHorizontal); + } + } +} diff --git a/Charts/Axes/LabelProvider.cs b/Charts/Axes/LabelProvider.cs new file mode 100644 index 0000000..10152d4 --- /dev/null +++ b/Charts/Axes/LabelProvider.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + public abstract class LabelProvider : LabelProviderBase + { + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + var ticks = ticksInfo.Ticks; + + UIElement[] res = new UIElement[ticks.Length]; + LabelTickInfo labelInfo = new LabelTickInfo { Info = ticksInfo.Info }; + + for (int i = 0; i < res.Length; i++) + { + labelInfo.Tick = ticks[i]; + labelInfo.Index = i; + + string labelText = GetString(labelInfo); + + TextBlock label = (TextBlock)GetResourceFromPool(); + if (label == null) + { + label = new TextBlock(); + } + + label.Text = labelText; + label.ToolTip = ticks[i].ToString(); + + res[i] = label; + + ApplyCustomView(labelInfo, label); + } + + return res; + } + } +} diff --git a/Charts/Axes/LabelProviderBase.cs b/Charts/Axes/LabelProviderBase.cs new file mode 100644 index 0000000..8414b89 --- /dev/null +++ b/Charts/Axes/LabelProviderBase.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.Common; +using System.ComponentModel; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + /// + /// Contains data for custom generation of tick's label. + /// + /// Type of ticks + public sealed class LabelTickInfo + { + internal LabelTickInfo() { } + + /// + /// Gets or sets the tick. + /// + /// The tick. + public T Tick { get; internal set; } + /// + /// Gets or sets additional info about ticks range. + /// + /// The info. + public object Info { get; internal set; } + /// + /// Gets or sets the index of tick in ticks array. + /// + /// The index. + public int Index { get; internal set; } + } + + /// + /// Base class for all label providers. + /// Contains a number of properties that can be used to adjust generated labels. + /// + /// Type of ticks, which labels are generated for + /// + /// Order of apllication of custom label string properties: + /// If CustomFormatter is not null, it is called first. + /// Then, if it was null or if it returned null string, + /// virtual GetStringCore method is called. It can be overloaded in subclasses. GetStringCore should not return null. + /// Then if LabelStringFormat is not null, it is applied. + /// After label's UI was created, you can change it by setting CustomView delegate - it allows you to adjust + /// UI properties of label. Note: not all labelProviders takes CustomView into account. + /// + public abstract class LabelProviderBase + { + + #region Private + + private string labelStringFormat = null; + private Func, string> customFormatter = null; + private Action, UIElement> customView = null; + + #endregion + + private static readonly UIElement[] emptyLabelsArray = new UIElement[0]; + protected static UIElement[] EmptyLabelsArray + { + get { return emptyLabelsArray; } + } + + /// + /// Creates labels by given ticks info. + /// Is not intended to be called from your code. + /// + /// The ticks info. + /// Array of s, which are axis labels for specified axis ticks. + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract UIElement[] CreateLabels(ITicksInfo ticksInfo); + + /// + /// Gets or sets the label string format. + /// + /// The label string format. + public string LabelStringFormat + { + get { return labelStringFormat; } + set + { + if (labelStringFormat != value) + { + labelStringFormat = value; + RaiseChanged(); + } + } + } + + /// + /// Gets or sets the custom formatter - delegate that can be called to create custom string representation of tick. + /// + /// The custom formatter. + public Func, string> CustomFormatter + { + get { return customFormatter; } + set + { + if (customFormatter != value) + { + customFormatter = value; + RaiseChanged(); + } + } + } + + /// + /// Gets or sets the custom view - delegate that is used to create a custom, non-default look of axis label. + /// Can be used to adjust some UI properties of generated label. + /// + /// The custom view. + public Action, UIElement> CustomView + { + get { return customView; } + set + { + if (customView != value) + { + customView = value; + RaiseChanged(); + } + } + } + + /// + /// Sets the custom formatter. + /// This is alternative to CustomFormatter property setter, the only difference is that Visual Studio shows + /// more convenient tooltip for methods rather than for properties' setters. + /// + /// The formatter. + public void SetCustomFormatter(Func, string> formatter) + { + CustomFormatter = formatter; + } + + /// + /// Sets the custom view. + /// This is alternative to CustomView property setter, the only difference is that Visual Studio shows + /// more convenient tooltip for methods rather than for properties' setters. + /// + /// The view. + public void SetCustomView(Action, UIElement> view) + { + CustomView = view; + } + + protected virtual string GetString(LabelTickInfo tickInfo) + { + string text = null; + if (CustomFormatter != null) + { + text = CustomFormatter(tickInfo); + } + if (text == null) + { + text = GetStringCore(tickInfo); + + if (text == null) + throw new ArgumentNullException(Strings.Exceptions.TextOfTickShouldNotBeNull); + } + if (LabelStringFormat != null) + { + text = String.Format(LabelStringFormat, text); + } + + return text; + } + + protected virtual string GetStringCore(LabelTickInfo tickInfo) + { + return tickInfo.Tick.ToString(); + } + + protected void ApplyCustomView(LabelTickInfo info, UIElement label) + { + if (CustomView != null) + { + CustomView(info, label); + } + } + + /// + /// Occurs when label provider is changed. + /// Notifies axis to update its view. + /// + public event EventHandler Changed; + protected void RaiseChanged() + { + Changed.Raise(this); + } + + private readonly ResourcePool pool = new ResourcePool(); + internal void ReleaseLabel(UIElement label) + { + if (ReleaseCore(label)) + { + pool.Put(label); + } + } + + protected virtual bool ReleaseCore(UIElement label) { return false; } + + protected UIElement GetResourceFromPool() + { + return pool.Get(); + } + } +} diff --git a/Charts/Axes/LabelProviderProperties.cs b/Charts/Axes/LabelProviderProperties.cs new file mode 100644 index 0000000..66dc487 --- /dev/null +++ b/Charts/Axes/LabelProviderProperties.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + internal class LabelProviderProperties : DependencyObject + { + public static bool GetExponentialIsCommonLabel(DependencyObject obj) + { + return (bool)obj.GetValue(ExponentialIsCommonLabelProperty); + } + + public static void SetExponentialIsCommonLabel(DependencyObject obj, bool value) + { + obj.SetValue(ExponentialIsCommonLabelProperty, value); + } + + public static readonly DependencyProperty ExponentialIsCommonLabelProperty = DependencyProperty.RegisterAttached( + "ExponentialIsCommonLabel", + typeof(bool), + typeof(LabelProviderProperties), + new FrameworkPropertyMetadata(true)); + } +} diff --git a/Charts/Axes/Numeric/CustomBaseNumericLabelProvider.cs b/Charts/Axes/Numeric/CustomBaseNumericLabelProvider.cs new file mode 100644 index 0000000..fe70df1 --- /dev/null +++ b/Charts/Axes/Numeric/CustomBaseNumericLabelProvider.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.Numeric +{ + public class CustomBaseNumericLabelProvider : LabelProvider + { + private double customBase = 2; + /// + /// Gets or sets the custom base. + /// + /// The custom base. + public double CustomBase + { + get { return customBase; } + set + { + if (Double.IsNaN(value)) + throw new ArgumentException(Strings.Exceptions.CustomBaseTicksProviderBaseIsNaN); + if (value <= 0) + throw new ArgumentOutOfRangeException(Strings.Exceptions.CustomBaseTicksProviderBaseIsTooSmall); + + customBase = value; + } + } + + /// + /// Initializes a new instance of the class. + /// + public CustomBaseNumericLabelProvider() { } + + /// + /// Initializes a new instance of the class. + /// + public CustomBaseNumericLabelProvider(double customBase) + : this() + { + CustomBase = customBase; + } + + /// + /// Initializes a new instance of the class. + /// + /// The custom base. + /// The custom base string. + public CustomBaseNumericLabelProvider(double customBase, string customBaseString) + : this(customBase) + { + CustomBaseString = customBaseString; + } + + private string customBaseString = null; + /// + /// Gets or sets the custom base string. + /// + /// The custom base string. + public string CustomBaseString + { + get { return customBaseString; } + set + { + if (customBaseString != value) + { + customBaseString = value; + RaiseChanged(); + } + } + } + + protected override string GetStringCore(LabelTickInfo tickInfo) + { + double value = tickInfo.Tick / customBase; + + string customBaseStr = customBaseString ?? customBase.ToString(); + string result; + if (value == 1) + result = customBaseStr; + else if (value == -1) + { + result = "-" + customBaseStr; + } + else + result = value.ToString() + customBaseStr; + + return result; + } + } +} diff --git a/Charts/Axes/Numeric/CustomBaseNumericTicksProvider.cs b/Charts/Axes/Numeric/CustomBaseNumericTicksProvider.cs new file mode 100644 index 0000000..765cc04 --- /dev/null +++ b/Charts/Axes/Numeric/CustomBaseNumericTicksProvider.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using System.Windows.Markup; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.Numeric +{ + [ContentProperty("TicksProvider")] + public class CustomBaseNumericTicksProvider : ITicksProvider + { + private double customBase = 2; + + /// + /// Gets or sets the custom base. + /// + /// The custom base. + public double CustomBase + { + get { return customBase; } + set + { + if (Double.IsNaN(value)) + throw new ArgumentException(Strings.Exceptions.CustomBaseTicksProviderBaseIsNaN); + if (value <= 0) + throw new ArgumentOutOfRangeException(Strings.Exceptions.CustomBaseTicksProviderBaseIsTooSmall); + + customBase = value; + } + } + + /// + /// Initializes a new instance of the class. + /// + public CustomBaseNumericTicksProvider() : this(2.0) { } + + /// + /// Initializes a new instance of the class. + /// + /// The custom base, e.g. Math.PI + public CustomBaseNumericTicksProvider(double customBase) : this(customBase, new NumericTicksProvider()) { } + + private CustomBaseNumericTicksProvider(double customBase, ITicksProvider ticksProvider) + { + if (ticksProvider == null) + throw new ArgumentNullException("ticksProvider"); + + CustomBase = customBase; + + TicksProvider = ticksProvider; + } + + private void ticksProvider_Changed(object sender, EventArgs e) + { + Changed.Raise(this); + } + + private ITicksProvider ticksProvider = null; + public ITicksProvider TicksProvider + { + get { return ticksProvider; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (ticksProvider != null) + ticksProvider.Changed -= ticksProvider_Changed; + ticksProvider = value; + ticksProvider.Changed += ticksProvider_Changed; + + if (minorTicksProvider != null) + minorTicksProvider.Changed -= minorTicksProvider_Changed; + minorTicksProvider = new MinorProviderWrapper(this); + minorTicksProvider.Changed += minorTicksProvider_Changed; + + Changed.Raise(this); + } + } + + void minorTicksProvider_Changed(object sender, EventArgs e) + { + Changed.Raise(this); + } + + private Range TransformRange(Range range) + { + double min = range.Min / customBase; + double max = range.Max / customBase; + + return new Range(min, max); + } + + #region ITicksProvider Members + + private double[] tickMarks; + public ITicksInfo GetTicks(Range range, int ticksCount) + { + var ticks = ticksProvider.GetTicks(TransformRange(range), ticksCount); + + TransformTicks(ticks); + + tickMarks = ticks.Ticks; + + return ticks; + } + + private void TransformTicks(ITicksInfo ticks) + { + for (int i = 0; i < ticks.Ticks.Length; i++) + { + ticks.Ticks[i] *= customBase; + } + } + + public int DecreaseTickCount(int ticksCount) + { + return ticksProvider.DecreaseTickCount(ticksCount); + } + + public int IncreaseTickCount(int ticksCount) + { + return ticksProvider.IncreaseTickCount(ticksCount); + } + + private ITicksProvider minorTicksProvider; + public ITicksProvider MinorProvider + { + get { return minorTicksProvider; } + } + + /// + /// Gets the major provider, used to generate major ticks - for example, years for common ticks as months. + /// + /// The major provider. + public ITicksProvider MajorProvider + { + get { return null; } + } + + public event EventHandler Changed; + + #endregion + + private sealed class MinorProviderWrapper : ITicksProvider + { + private readonly CustomBaseNumericTicksProvider owner; + + public MinorProviderWrapper(CustomBaseNumericTicksProvider owner) + { + this.owner = owner; + + MinorTicksProvider.Changed += MinorTicksProvider_Changed; + } + + private void MinorTicksProvider_Changed(object sender, EventArgs e) + { + Changed.Raise(this); + } + + private ITicksProvider MinorTicksProvider + { + get { return owner.ticksProvider.MinorProvider; } + } + + #region ITicksProvider Members + + public ITicksInfo GetTicks(Range range, int ticksCount) + { + var minorProvider = MinorTicksProvider; + var ticks = minorProvider.GetTicks(range, ticksCount); + + return ticks; + } + + public int DecreaseTickCount(int ticksCount) + { + return MinorTicksProvider.DecreaseTickCount(ticksCount); + } + + public int IncreaseTickCount(int ticksCount) + { + return MinorTicksProvider.IncreaseTickCount(ticksCount); + } + + public ITicksProvider MinorProvider + { + get { return MinorTicksProvider.MinorProvider; } + } + + public ITicksProvider MajorProvider + { + get { return owner; } + } + + public event EventHandler Changed; + + #endregion + } + } +} diff --git a/Charts/Axes/Numeric/ExponentialLabelProvider.cs b/Charts/Axes/Numeric/ExponentialLabelProvider.cs new file mode 100644 index 0000000..1f89b7c --- /dev/null +++ b/Charts/Axes/Numeric/ExponentialLabelProvider.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Globalization; +using System.Diagnostics; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents an axis label provider for double ticks, generating labels with numbers in exponential form when it is appropriate. + /// + public sealed class ExponentialLabelProvider : NumericLabelProviderBase + { + /// + /// Initializes a new instance of the class. + /// + public ExponentialLabelProvider() { } + + /// + /// Creates labels by given ticks info. + /// Is not intended to be called from your code. + /// + /// The ticks info. + /// + /// Array of s, which are axis labels for specified axis ticks. + /// + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + var ticks = ticksInfo.Ticks; + + Init(ticks); + + UIElement[] res = new UIElement[ticks.Length]; + + LabelTickInfo tickInfo = new LabelTickInfo { Info = ticksInfo.Info }; + + for (int i = 0; i < res.Length; i++) + { + var tick = ticks[i]; + tickInfo.Tick = tick; + tickInfo.Index = i; + + string labelText = GetString(tickInfo); + + TextBlock label; + if (labelText.Contains('E')) + { + string[] substrs = labelText.Split('E'); + string mantissa = substrs[0]; + string exponenta = substrs[1]; + exponenta = exponenta.TrimStart('+'); + Span span = new Span(); + span.Inlines.Add(String.Format(CultureInfo.CurrentCulture, "{0}·10", mantissa)); + Span exponentaSpan = new Span(new Run(exponenta)); + exponentaSpan.BaselineAlignment = BaselineAlignment.Superscript; + exponentaSpan.FontSize = 8; + span.Inlines.Add(exponentaSpan); + + label = new TextBlock(span); + LabelProviderProperties.SetExponentialIsCommonLabel(label, false); + } + else + { + label = (TextBlock)GetResourceFromPool(); + if (label == null) + { + label = new TextBlock(); + } + + label.Text = labelText; + } + res[i] = label; + label.ToolTip = tick.ToString(CultureInfo.CurrentCulture); + + ApplyCustomView(tickInfo, label); + } + + return res; + } + + protected override bool ReleaseCore(UIElement label) + { + bool isNotExponential = LabelProviderProperties.GetExponentialIsCommonLabel(label); + return isNotExponential && CustomView == null; + } + } +} diff --git a/Charts/Axes/Numeric/HorizontalAxis.cs b/Charts/Axes/Numeric/HorizontalAxis.cs new file mode 100644 index 0000000..3e7030a --- /dev/null +++ b/Charts/Axes/Numeric/HorizontalAxis.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a horizontal axis with values of type. + /// Can be placed only from bottom or top side of plotter. + /// By default is placed from the bottom side. + /// + public class HorizontalAxis : NumericAxis + { + /// + /// Initializes a new instance of the class, placed on bottom of . + /// + public HorizontalAxis() + { + Placement = AxisPlacement.Bottom; + } + + /// + /// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc. + /// If proposed placement is wrong, throws an ArgumentException. + /// + /// The new placement. + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Left || newPlacement == AxisPlacement.Right) + throw new ArgumentException(Strings.Exceptions.HorizontalAxisCannotBeVertical); + } + } +} diff --git a/Charts/Axes/Numeric/LogarithmNumericTicksProvider.cs b/Charts/Axes/Numeric/LogarithmNumericTicksProvider.cs new file mode 100644 index 0000000..48adfb1 --- /dev/null +++ b/Charts/Axes/Numeric/LogarithmNumericTicksProvider.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Markup; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.Numeric +{ + /// + /// Represents a ticks provider for logarithmically transfomed axis - returns ticks which are a power of specified logarithm base. + /// + public class LogarithmNumericTicksProvider : ITicksProvider + { + /// + /// Initializes a new instance of the class. + /// + public LogarithmNumericTicksProvider() + { + minorProvider = new MinorNumericTicksProvider(this); + minorProvider.Changed += ticksProvider_Changed; + } + + /// + /// Initializes a new instance of the class. + /// + /// The logarithm base. + public LogarithmNumericTicksProvider(double logarithmBase) + : this() + { + LogarithmBase = logarithmBase; + } + + private void ticksProvider_Changed(object sender, EventArgs e) + { + Changed.Raise(this); + } + + private double logarithmBase = 10; + public double LogarithmBase + { + get { return logarithmBase; } + set + { + if (value <= 0) + throw new ArgumentOutOfRangeException(Strings.Exceptions.LogarithmBaseShouldBePositive); + + logarithmBase = value; + } + } + + private double LogByBase(double d) + { + return Math.Log10(d) / Math.Log10(logarithmBase); + } + + #region ITicksProvider Members + + private double[] ticks; + public ITicksInfo GetTicks(Range range, int ticksCount) + { + double min = LogByBase(range.Min); + double max = LogByBase(range.Max); + + double minDown = Math.Floor(min); + double maxUp = Math.Ceiling(max); + + double logLength = LogByBase(range.GetLength()); + + ticks = CreateTicks(range); + + int log = RoundingHelper.GetDifferenceLog(range.Min, range.Max); + TicksInfo result = new TicksInfo { Ticks = ticks, TickSizes = ArrayExtensions.CreateArray(ticks.Length, 1.0), Info = log }; + return result; + } + + private double[] CreateTicks(Range range) + { + double min = LogByBase(range.Min); + double max = LogByBase(range.Max); + + double minDown = Math.Floor(min); + double maxUp = Math.Ceiling(max); + + int intStart = (int)Math.Floor(minDown); + int count = (int)(maxUp - minDown + 1); + + var ticks = new double[count]; + for (int i = 0; i < count; i++) + { + ticks[i] = intStart + i; + } + + for (int i = 0; i < ticks.Length; i++) + { + ticks[i] = Math.Pow(logarithmBase, ticks[i]); + } + + return ticks; + } + + public int DecreaseTickCount(int ticksCount) + { + return ticksCount; + } + + public int IncreaseTickCount(int ticksCount) + { + return ticksCount; + } + + private MinorNumericTicksProvider minorProvider; + public ITicksProvider MinorProvider + { + get + { + minorProvider.SetRanges(ArrayExtensions.GetPairs(ticks)); + return minorProvider; + } + } + + public ITicksProvider MajorProvider + { + get { return null; } + } + + public event EventHandler Changed; + + #endregion + } +} diff --git a/Charts/Axes/Numeric/MinorNumericTicksProvider.cs b/Charts/Axes/Numeric/MinorNumericTicksProvider.cs new file mode 100644 index 0000000..122d155 --- /dev/null +++ b/Charts/Axes/Numeric/MinorNumericTicksProvider.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public sealed class MinorNumericTicksProvider : ITicksProvider + { + private readonly ITicksProvider parent; + private Range[] ranges; + internal void SetRanges(IEnumerable> ranges) + { + this.ranges = ranges.ToArray(); + } + + private double[] coeffs; + public double[] Coeffs + { + get { return coeffs; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + coeffs = value; + Changed.Raise(this); + } + } + + internal MinorNumericTicksProvider(ITicksProvider parent) + { + this.parent = parent; + Coeffs = new double[] { 0.3, 0.3, 0.3, 0.3, 0.6, 0.3, 0.3, 0.3, 0.3 }; + } + + #region ITicksProvider Members + + public event EventHandler Changed; + + public ITicksInfo GetTicks(Range range, int ticksCount) + { + if (Coeffs.Length == 0) + return new TicksInfo(); + + var minorTicks = ranges.Select(r => CreateTicks(r)).SelectMany(m => m); + var res = new TicksInfo(); + res.TickSizes = minorTicks.Select(m => m.Value).ToArray(); + res.Ticks = minorTicks.Select(m => m.Tick).ToArray(); + + return res; + } + + public MinorTickInfo[] CreateTicks(Range range) + { + double step = (range.Max - range.Min) / (Coeffs.Length + 1); + + MinorTickInfo[] res = new MinorTickInfo[Coeffs.Length]; + for (int i = 0; i < Coeffs.Length; i++) + { + res[i] = new MinorTickInfo(Coeffs[i], range.Min + step * (i + 1)); + } + return res; + } + + public int DecreaseTickCount(int ticksCount) + { + return ticksCount; + } + + public int IncreaseTickCount(int ticksCount) + { + return ticksCount; + } + + public ITicksProvider MinorProvider + { + get { return null; } + } + + public ITicksProvider MajorProvider + { + get { return parent; } + } + + #endregion + } +} diff --git a/Charts/Axes/Numeric/NumericAxis.cs b/Charts/Axes/Numeric/NumericAxis.cs new file mode 100644 index 0000000..a2f692b --- /dev/null +++ b/Charts/Axes/Numeric/NumericAxis.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Media; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes.Numeric; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a numeric axis with values of type. + /// + public class NumericAxis : AxisBase + { + /// + /// Initializes a new instance of the class. + /// + public NumericAxis() + : base(new NumericAxisControl(), + d => d, + d => d) + { + } + + /// + /// Sets conversions of axis - functions used to convert values of axis type to and from double values of viewport. + /// Sets both ConvertToDouble and ConvertFromDouble properties. + /// + /// The minimal viewport value. + /// The value of axis type, corresponding to minimal viewport value. + /// The maximal viewport value. + /// The value of axis type, corresponding to maximal viewport value. + public override void SetConversion(double min, double minValue, double max, double maxValue) + { + var conversion = new NumericConversion(min, minValue, max, maxValue); + + this.ConvertFromDouble = conversion.FromDouble; + this.ConvertToDouble = conversion.ToDouble; + } + } +} diff --git a/Charts/Axes/Numeric/NumericAxisControl.cs b/Charts/Axes/Numeric/NumericAxisControl.cs new file mode 100644 index 0000000..b3d3e85 --- /dev/null +++ b/Charts/Axes/Numeric/NumericAxisControl.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class NumericAxisControl : AxisControl + { + public NumericAxisControl() + { + LabelProvider = new ExponentialLabelProvider(); + TicksProvider = new NumericTicksProvider(); + ConvertToDouble = d => d; + Range = new Range(0, 10); + } + } +} diff --git a/Charts/Axes/Numeric/NumericConversion.cs b/Charts/Axes/Numeric/NumericConversion.cs new file mode 100644 index 0000000..1800e45 --- /dev/null +++ b/Charts/Axes/Numeric/NumericConversion.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.Numeric +{ + internal sealed class NumericConversion + { + private readonly double min; + private readonly double length; + private readonly double minValue; + private readonly double valueLength; + + public NumericConversion(double min, double minValue, double max, double maxValue) + { + this.min = min; + this.length = max - min; + + this.minValue = minValue; + this.valueLength = maxValue - minValue; + } + + public double FromDouble(double value) + { + double ratio = (value - min) / length; + + return minValue + ratio * valueLength; + } + + public double ToDouble(double value) + { + double ratio = (value - minValue) / valueLength; + + return min + length * ratio; + } + } +} diff --git a/Charts/Axes/Numeric/NumericLabelProviderBase.cs b/Charts/Axes/Numeric/NumericLabelProviderBase.cs new file mode 100644 index 0000000..76e2caa --- /dev/null +++ b/Charts/Axes/Numeric/NumericLabelProviderBase.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public abstract class NumericLabelProviderBase : LabelProviderBase + { + bool shouldRound = true; + private int rounding; + protected void Init(double[] ticks) + { + if (ticks.Length == 0) + return; + + double start = ticks[0]; + double finish = ticks[ticks.Length - 1]; + + if (start == finish) + { + shouldRound = false; + return; + } + + double delta = finish - start; + + rounding = (int)Math.Round(Math.Log10(delta)); + + double newStart = RoundingHelper.Round(start, rounding); + double newFinish = RoundingHelper.Round(finish, rounding); + if (newStart == newFinish) + rounding--; + } + + protected override string GetStringCore(LabelTickInfo tickInfo) + { + string res; + if (!shouldRound) + { + res = tickInfo.Tick.ToString(); + } + else + { + int round = Math.Min(15, Math.Max(-15, rounding - 3)); // was rounding - 2 + res = RoundingHelper.Round(tickInfo.Tick, round).ToString(); + } + + return res; + } + } +} diff --git a/Charts/Axes/Numeric/NumericTicksProvider.cs b/Charts/Axes/Numeric/NumericTicksProvider.cs new file mode 100644 index 0000000..495bbd6 --- /dev/null +++ b/Charts/Axes/Numeric/NumericTicksProvider.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using System.Collections.ObjectModel; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a ticks provider for values. + /// + public sealed class NumericTicksProvider : ITicksProvider + { + /// + /// Initializes a new instance of the class. + /// + public NumericTicksProvider() + { + minorProvider = new MinorNumericTicksProvider(this); + minorProvider.Changed += minorProvider_Changed; + minorProvider.Coeffs = new double[] { 0.3, 0.3, 0.3, 0.3, 0.6, 0.3, 0.3, 0.3, 0.3 }; + } + + private void minorProvider_Changed(object sender, EventArgs e) + { + Changed.Raise(this); + } + + public event EventHandler Changed; + private void RaiseChangedEvent() + { + Changed.Raise(this); + } + + private double minStep = 0.0; + /// + /// Gets or sets the minimal step between ticks. + /// + /// The min step. + public double MinStep + { + get { return minStep; } + set + { + Verify.IsTrue(value >= 0.0, "value"); + if (minStep != value) + { + minStep = value; + RaiseChangedEvent(); + } + } + } + + private double[] ticks; + public ITicksInfo GetTicks(Range range, int ticksCount) + { + double start = range.Min; + double finish = range.Max; + + double delta = finish - start; + + int log = (int)Math.Round(Math.Log10(delta)); + + double newStart = RoundingHelper.Round(start, log); + double newFinish = RoundingHelper.Round(finish, log); + if (newStart == newFinish) + { + log--; + newStart = RoundingHelper.Round(start, log); + newFinish = RoundingHelper.Round(finish, log); + } + + // calculating step between ticks + double unroundedStep = (newFinish - newStart) / ticksCount; + int stepLog = log; + // trying to round step + double step = RoundingHelper.Round(unroundedStep, stepLog); + if (step == 0) + { + stepLog--; + step = RoundingHelper.Round(unroundedStep, stepLog); + if (step == 0) + { + // step will not be rounded if attempts to be rounded to zero. + step = unroundedStep; + } + } + + if (step < minStep) + step = minStep; + + if (step != 0.0) + { + ticks = CreateTicks(start, finish, step); + } + else + { + ticks = new double[] { }; + } + + TicksInfo res = new TicksInfo { Info = log, Ticks = ticks }; + + return res; + } + + private static double[] CreateTicks(double start, double finish, double step) + { + DebugVerify.Is(step != 0.0); + + double x = step * Math.Floor(start / step); + + if (x == x + step) + { + return new double[0]; + } + + List res = new List(); + + double increasedFinish = finish + step * 1.05; + while (x <= increasedFinish) + { + res.Add(x); + DebugVerify.Is(res.Count < 2000); + x += step; + } + return res.ToArray(); + } + + private static int[] tickCounts = new int[] { 20, 10, 5, 4, 2, 1 }; + + public const int DefaultPreferredTicksCount = 10; + + public int DecreaseTickCount(int ticksCount) + { + return tickCounts.FirstOrDefault(tick => tick < ticksCount); + } + + public int IncreaseTickCount(int ticksCount) + { + int newTickCount = tickCounts.Reverse().FirstOrDefault(tick => tick > ticksCount); + if (newTickCount == 0) + newTickCount = tickCounts[0]; + + return newTickCount; + } + + private readonly MinorNumericTicksProvider minorProvider; + public ITicksProvider MinorProvider + { + get + { + if (ticks != null) + { + minorProvider.SetRanges(ticks.GetPairs()); + } + + return minorProvider; + } + } + + public ITicksProvider MajorProvider + { + get { return null; } + } + } +} diff --git a/Charts/Axes/Numeric/ToStringLabelProvider.cs b/Charts/Axes/Numeric/ToStringLabelProvider.cs new file mode 100644 index 0000000..60caa1e --- /dev/null +++ b/Charts/Axes/Numeric/ToStringLabelProvider.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; +using System.Globalization; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a simple label provider for double ticks, which simply returns result of .ToString() method, called for rounded ticks. + /// + public class ToStringLabelProvider : NumericLabelProviderBase + { + /// + /// Initializes a new instance of the class. + /// + public ToStringLabelProvider() { } + + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + var ticks = ticksInfo.Ticks; + + Init(ticks); + + UIElement[] res = new UIElement[ticks.Length]; + LabelTickInfo tickInfo = new LabelTickInfo { Info = ticksInfo.Info }; + for (int i = 0; i < res.Length; i++) + { + tickInfo.Tick = ticks[i]; + tickInfo.Index = i; + + string labelText = GetString(tickInfo); + + TextBlock label = (TextBlock)GetResourceFromPool(); + if (label == null) + { + label = new TextBlock(); + } + + label.Text = labelText; + label.ToolTip = ticks[i].ToString(); + + res[i] = label; + + ApplyCustomView(tickInfo, label); + } + return res; + } + } +} diff --git a/Charts/Axes/Numeric/UnroundingLabelProvider.cs b/Charts/Axes/Numeric/UnroundingLabelProvider.cs new file mode 100644 index 0000000..7c63dfe --- /dev/null +++ b/Charts/Axes/Numeric/UnroundingLabelProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes.Numeric +{ + public class UnroundingLabelProvider : LabelProvider + { + } +} diff --git a/Charts/Axes/Numeric/VerticalAxis.cs b/Charts/Axes/Numeric/VerticalAxis.cs new file mode 100644 index 0000000..37c236b --- /dev/null +++ b/Charts/Axes/Numeric/VerticalAxis.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Media; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a vertical axis with values of System.Double type. + /// Can be placed only from left or right side of plotter. + /// By default is placed from the left side. + /// + public class VerticalAxis : NumericAxis + { + /// + /// Initializes a new instance of the class. + /// + public VerticalAxis() + { + Placement = AxisPlacement.Left; + } + + /// + /// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc. + /// If proposed placement if wrong, throws an ArgumentException. + /// + /// The new placement. + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Bottom || newPlacement == AxisPlacement.Top) + throw new ArgumentException(Strings.Exceptions.VerticalAxisCannotBeHorizontal); + } + } +} diff --git a/Charts/Axes/Numeric/VerticalNumericAxis.cs b/Charts/Axes/Numeric/VerticalNumericAxis.cs new file mode 100644 index 0000000..a967e2e --- /dev/null +++ b/Charts/Axes/Numeric/VerticalNumericAxis.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Axes +{ + public class VerticalNumericAxis : NumericAxis + { + public VerticalNumericAxis() + { + Placement = AxisPlacement.Left; + } + + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Bottom || newPlacement == AxisPlacement.Top) + throw new ArgumentException(Strings.Exceptions.VerticalAxisCannotBeHorizontal); + } + } +} diff --git a/Charts/Axes/RoundingHelper.cs b/Charts/Axes/RoundingHelper.cs new file mode 100644 index 0000000..856e211 --- /dev/null +++ b/Charts/Axes/RoundingHelper.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal static class RoundingHelper + { + internal static int GetDifferenceLog(double min, double max) + { + return (int)Math.Round(Math.Log10(Math.Abs(max - min))); + } + + internal static double Round(double number, int rem) + { + if (rem <= 0) + { + rem = MathHelper.Clamp(-rem, 0, 15); + return Math.Round(number, rem); + } + else + { + double pow = Math.Pow(10, rem - 1); + double val = pow * Math.Round(number / Math.Pow(10, rem - 1)); + return val; + } + } + + internal static double Round(double value, Range range) + { + int log = GetDifferenceLog(range.Min, range.Max); + + return Round(value, log); + } + + internal static RoundingInfo CreateRoundedRange(double min, double max) + { + double delta = max - min; + + if (delta == 0) + return new RoundingInfo { Min = min, Max = max, Log = 0 }; + + int log = (int)Math.Round(Math.Log10(Math.Abs(delta))) + 1; + + double newMin = Round(min, log); + double newMax = Round(max, log); + if (newMin == newMax) + { + log--; + newMin = Round(min, log); + newMax = Round(max, log); + } + + return new RoundingInfo { Min = newMin, Max = newMax, Log = log }; + } + } + + [DebuggerDisplay("{Min} - {Max}, Log = {Log}")] + internal sealed class RoundingInfo + { + public double Min { get; set; } + public double Max { get; set; } + public int Log { get; set; } + } +} diff --git a/Charts/Axes/StackCanvas.cs b/Charts/Axes/StackCanvas.cs new file mode 100644 index 0000000..d03d3c9 --- /dev/null +++ b/Charts/Axes/StackCanvas.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows; +using System.Windows.Media; +using System.Diagnostics; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class StackCanvas : Panel + { + public StackCanvas() + { + //ClipToBounds = true; + } + + #region EndCoordinate attached property + + [AttachedPropertyBrowsableForChildren] + public static double GetEndCoordinate(DependencyObject obj) + { + return (double)obj.GetValue(EndCoordinateProperty); + } + + public static void SetEndCoordinate(DependencyObject obj, double value) + { + obj.SetValue(EndCoordinateProperty, value); + } + + public static readonly DependencyProperty EndCoordinateProperty = DependencyProperty.RegisterAttached( + "EndCoordinate", + typeof(double), + typeof(StackCanvas), + new PropertyMetadata(Double.NaN, OnCoordinateChanged)); + + #endregion + + #region Coordinate attached property + + [AttachedPropertyBrowsableForChildren] + public static double GetCoordinate(DependencyObject obj) + { + return (double)obj.GetValue(CoordinateProperty); + } + + public static void SetCoordinate(DependencyObject obj, double value) + { + obj.SetValue(CoordinateProperty, value); + } + + public static readonly DependencyProperty CoordinateProperty = DependencyProperty.RegisterAttached( + "Coordinate", + typeof(double), + typeof(StackCanvas), + new PropertyMetadata(0.0, OnCoordinateChanged)); + + private static void OnCoordinateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UIElement reference = d as UIElement; + if (reference != null) + { + StackCanvas parent = VisualTreeHelper.GetParent(reference) as StackCanvas; + if (parent != null) + { + parent.InvalidateArrange(); + } + } + } + #endregion + + #region AxisPlacement property + + public AxisPlacement Placement + { + get { return (AxisPlacement)GetValue(PlacementProperty); } + set { SetValue(PlacementProperty, value); } + } + + public static readonly DependencyProperty PlacementProperty = + DependencyProperty.Register( + "Placement", + typeof(AxisPlacement), + typeof(StackCanvas), + new FrameworkPropertyMetadata( + AxisPlacement.Bottom, + FrameworkPropertyMetadataOptions.AffectsArrange)); + + #endregion + + private bool IsHorizontal + { + get { return Placement == AxisPlacement.Top || Placement == AxisPlacement.Bottom; } + } + + protected override Size MeasureOverride(Size constraint) + { + Size availableSize = constraint; + Size size = new Size(); + + bool isHorizontal = IsHorizontal; + + if (isHorizontal) + { + availableSize.Width = Double.PositiveInfinity; + size.Width = constraint.Width; + } + else + { + availableSize.Height = Double.PositiveInfinity; + size.Height = constraint.Height; + } + + // measuring all children and determinimg self width and height + foreach (UIElement element in base.Children) + { + if (element != null) + { + Size childSize = GetChildSize(element, availableSize); + element.Measure(childSize); + Size desiredSize = element.DesiredSize; + + if (isHorizontal) + { + size.Height = Math.Max(size.Height, desiredSize.Height); + } + else + { + size.Width = Math.Max(size.Width, desiredSize.Width); + } + } + } + + if (Double.IsPositiveInfinity(size.Width)) size.Width = 0; + if (Double.IsPositiveInfinity(size.Height)) size.Height = 0; + + return size; + } + + private Size GetChildSize(UIElement element, Size availableSize) + { + var coordinate = GetCoordinate(element); + var endCoordinate = GetEndCoordinate(element); + + if (coordinate.IsNotNaN() && endCoordinate.IsNotNaN()) + { + if (Placement.IsBottomOrTop()) + { + availableSize.Width = endCoordinate - coordinate; + } + else + { + availableSize.Height = endCoordinate - coordinate; + } + } + + return availableSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + bool isHorizontal = IsHorizontal; + + foreach (FrameworkElement element in base.Children) + { + if (element == null) + { + continue; + } + + Size elementSize = element.DesiredSize; + double x = 0.0; + double y = 0.0; + + switch (Placement) + { + case AxisPlacement.Left: + x = finalSize.Width - elementSize.Width; + break; + case AxisPlacement.Right: + x = 0; + break; + case AxisPlacement.Top: + y = finalSize.Height - elementSize.Height; + break; + case AxisPlacement.Bottom: + y = 0; + break; + default: + break; + } + + double coordinate = GetCoordinate(element); + + if (!Double.IsNaN(GetEndCoordinate(element))) + { + double endCoordinate = GetEndCoordinate(element); + double size = endCoordinate - coordinate; + if (size < 0) + { + size = -size; + coordinate -= size; + } + if (isHorizontal) + elementSize.Width = size; + else + elementSize.Height = size; + } + + + // shift for common tick labels, not for major ones. + if (isHorizontal) + { + x = coordinate; + if (element.HorizontalAlignment == HorizontalAlignment.Center) + x = coordinate - elementSize.Width / 2; + } + else + { + if (element.VerticalAlignment == VerticalAlignment.Center) + y = coordinate - elementSize.Height / 2; + else if (element.VerticalAlignment == VerticalAlignment.Bottom) + y = coordinate - elementSize.Height; + else if (element.VerticalAlignment == VerticalAlignment.Top) + y = coordinate; + } + + Rect bounds = new Rect(new Point(x, y), elementSize); + element.Arrange(bounds); + } + + return finalSize; + } + } +} diff --git a/Charts/Axes/TimeSpan/HorizontalTimeSpanAxis.cs b/Charts/Axes/TimeSpan/HorizontalTimeSpanAxis.cs new file mode 100644 index 0000000..2c801ee --- /dev/null +++ b/Charts/Axes/TimeSpan/HorizontalTimeSpanAxis.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a horizontal axis with values of type. + /// + public class HorizontalTimeSpanAxis : TimeSpanAxis + { + /// + /// Initializes a new instance of the class, placed on the bottom of . + /// + public HorizontalTimeSpanAxis() + { + Placement = AxisPlacement.Bottom; + } + + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Left || newPlacement == AxisPlacement.Right) + throw new ArgumentException(Strings.Exceptions.HorizontalAxisCannotBeVertical); + } + } +} diff --git a/Charts/Axes/TimeSpan/MinorTimeSpanProvider.cs b/Charts/Axes/TimeSpan/MinorTimeSpanProvider.cs new file mode 100644 index 0000000..3ad76dd --- /dev/null +++ b/Charts/Axes/TimeSpan/MinorTimeSpanProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal sealed class MinorTimeSpanTicksProvider : MinorTimeProviderBase + { + public MinorTimeSpanTicksProvider(ITicksProvider owner) : base(owner) { } + + protected override bool IsInside(TimeSpan value, Range range) + { + return range.Min < value && value < range.Max; + } + } +} diff --git a/Charts/Axes/TimeSpan/TimeSpanAxis.cs b/Charts/Axes/TimeSpan/TimeSpanAxis.cs new file mode 100644 index 0000000..e2a9b98 --- /dev/null +++ b/Charts/Axes/TimeSpan/TimeSpanAxis.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents axis with values of type TimeSpan. + /// + public class TimeSpanAxis : AxisBase + { + /// + /// Initializes a new instance of the class with default values conversion. + /// + public TimeSpanAxis() + : base(new TimeSpanAxisControl(), + DoubleToTimeSpan, TimeSpanToDouble) + { } + + private static readonly long minTicks = TimeSpan.MinValue.Ticks; + private static readonly long maxTicks = TimeSpan.MaxValue.Ticks; + private static TimeSpan DoubleToTimeSpan(double value) + { + long ticks = (long)(value * 10000000000L); + + // todo should we throw an exception if number of ticks is too big or small? + if (ticks < minTicks) + ticks = minTicks; + else if (ticks > maxTicks) + ticks = maxTicks; + + return new TimeSpan(ticks); + } + + private static double TimeSpanToDouble(TimeSpan time) + { + return time.Ticks / 10000000000.0; + } + + /// + /// Sets conversions of axis - functions used to convert values of axis type to and from double values of viewport. + /// Sets both ConvertToDouble and ConvertFromDouble properties. + /// + /// The minimal viewport value. + /// The value of axis type, corresponding to minimal viewport value. + /// The maximal viewport value. + /// The value of axis type, corresponding to maximal viewport value. + public override void SetConversion(double min, TimeSpan minValue, double max, TimeSpan maxValue) + { + var conversion = new TimeSpanToDoubleConversion(min, minValue, max, maxValue); + + ConvertToDouble = conversion.ToDouble; + ConvertFromDouble = conversion.FromDouble; + } + } +} diff --git a/Charts/Axes/TimeSpan/TimeSpanAxisControl.cs b/Charts/Axes/TimeSpan/TimeSpanAxisControl.cs new file mode 100644 index 0000000..96a802d --- /dev/null +++ b/Charts/Axes/TimeSpan/TimeSpanAxisControl.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class TimeSpanAxisControl : AxisControl + { + public TimeSpanAxisControl() + { + LabelProvider = new TimeSpanLabelProvider(); + TicksProvider = new TimeSpanTicksProvider(); + + ConvertToDouble = time => time.Ticks; + + Range = new Range(new TimeSpan(), new TimeSpan(1, 0, 0)); + } + } +} diff --git a/Charts/Axes/TimeSpan/TimeSpanLabelProvider.cs b/Charts/Axes/TimeSpan/TimeSpanLabelProvider.cs new file mode 100644 index 0000000..2e94c05 --- /dev/null +++ b/Charts/Axes/TimeSpan/TimeSpanLabelProvider.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using Microsoft.Research.DynamicDataDisplay.Charts.Axes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class TimeSpanLabelProvider : LabelProviderBase + { + public override UIElement[] CreateLabels(ITicksInfo ticksInfo) + { + object info = ticksInfo.Info; + var ticks = ticksInfo.Ticks; + + LabelTickInfo tickInfo = new LabelTickInfo(); + + UIElement[] res = new UIElement[ticks.Length]; + for (int i = 0; i < ticks.Length; i++) + { + tickInfo.Tick = ticks[i]; + tickInfo.Info = info; + + string tickText = GetString(tickInfo); + UIElement label = new TextBlock { Text = tickText, ToolTip = ticks[i] }; + res[i] = label; + } + return res; + } + } +} diff --git a/Charts/Axes/TimeSpan/TimeSpanTicksProvider.cs b/Charts/Axes/TimeSpan/TimeSpanTicksProvider.cs new file mode 100644 index 0000000..519f117 --- /dev/null +++ b/Charts/Axes/TimeSpan/TimeSpanTicksProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class TimeSpanTicksProvider : TimeTicksProviderBase + { + static TimeSpanTicksProvider() + { + Providers.Add(DifferenceIn.Year, new DayTimeSpanProvider()); + Providers.Add(DifferenceIn.Month, new DayTimeSpanProvider()); + Providers.Add(DifferenceIn.Day, new DayTimeSpanProvider()); + Providers.Add(DifferenceIn.Hour, new HourTimeSpanProvider()); + Providers.Add(DifferenceIn.Minute, new MinuteTimeSpanProvider()); + Providers.Add(DifferenceIn.Second, new SecondTimeSpanProvider()); + Providers.Add(DifferenceIn.Millisecond, new MillisecondTimeSpanProvider()); + + MinorProviders.Add(DifferenceIn.Year, new MinorTimeSpanTicksProvider(new DayTimeSpanProvider())); + MinorProviders.Add(DifferenceIn.Month, new MinorTimeSpanTicksProvider(new DayTimeSpanProvider())); + MinorProviders.Add(DifferenceIn.Day, new MinorTimeSpanTicksProvider(new DayTimeSpanProvider())); + MinorProviders.Add(DifferenceIn.Hour, new MinorTimeSpanTicksProvider(new HourTimeSpanProvider())); + MinorProviders.Add(DifferenceIn.Minute, new MinorTimeSpanTicksProvider(new MinuteTimeSpanProvider())); + MinorProviders.Add(DifferenceIn.Second, new MinorTimeSpanTicksProvider(new SecondTimeSpanProvider())); + MinorProviders.Add(DifferenceIn.Millisecond, new MinorTimeSpanTicksProvider(new MillisecondTimeSpanProvider())); + } + + protected override TimeSpan GetDifference(TimeSpan start, TimeSpan end) + { + return end - start; + } + } +} diff --git a/Charts/Axes/TimeSpan/TimeSpanTicksProviderBase.cs b/Charts/Axes/TimeSpan/TimeSpanTicksProviderBase.cs new file mode 100644 index 0000000..15583c1 --- /dev/null +++ b/Charts/Axes/TimeSpan/TimeSpanTicksProviderBase.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal abstract class TimeSpanTicksProviderBase : TimePeriodTicksProvider + { + protected sealed override bool Continue(TimeSpan current, TimeSpan end) + { + return current < end; + } + + protected sealed override TimeSpan RoundDown(TimeSpan start, TimeSpan end) + { + return RoundDown(start, Difference); + } + + protected sealed override TimeSpan RoundUp(TimeSpan start, TimeSpan end) + { + return RoundUp(end, Difference); + } + + protected static TimeSpan Shift(TimeSpan span, DifferenceIn diff) + { + TimeSpan res = span; + + TimeSpan shift = new TimeSpan(); + switch (diff) + { + case DifferenceIn.Year: + case DifferenceIn.Month: + case DifferenceIn.Day: + shift = TimeSpan.FromDays(1); + break; + case DifferenceIn.Hour: + shift = TimeSpan.FromHours(1); + break; + case DifferenceIn.Minute: + shift = TimeSpan.FromMinutes(1); + break; + case DifferenceIn.Second: + shift = TimeSpan.FromSeconds(1); + break; + case DifferenceIn.Millisecond: + shift = TimeSpan.FromMilliseconds(1); + break; + default: + break; + } + + res = res.Add(shift); + return res; + } + + protected sealed override TimeSpan RoundDown(TimeSpan timeSpan, DifferenceIn diff) + { + TimeSpan res = timeSpan; + + if (timeSpan.Ticks < 0) + { + res = RoundUp(timeSpan.Duration(), diff).Negate(); + } + else + { + switch (diff) + { + case DifferenceIn.Year: + case DifferenceIn.Month: + case DifferenceIn.Day: + res = TimeSpan.FromDays(timeSpan.Days); + break; + case DifferenceIn.Hour: + res = TimeSpan.FromDays(timeSpan.Days). + Add(TimeSpan.FromHours(timeSpan.Hours)); + break; + case DifferenceIn.Minute: + res = TimeSpan.FromDays(timeSpan.Days). + Add(TimeSpan.FromHours(timeSpan.Hours)). + Add(TimeSpan.FromMinutes(timeSpan.Minutes)); + break; + case DifferenceIn.Second: + res = TimeSpan.FromDays(timeSpan.Days). + Add(TimeSpan.FromHours(timeSpan.Hours)). + Add(TimeSpan.FromMinutes(timeSpan.Minutes)). + Add(TimeSpan.FromSeconds(timeSpan.Seconds)); + break; + case DifferenceIn.Millisecond: + res = timeSpan; + break; + default: + break; + } + } + + return res; + } + + protected sealed override TimeSpan RoundUp(TimeSpan dateTime, DifferenceIn diff) + { + TimeSpan res = RoundDown(dateTime, diff); + res = Shift(res, diff); + + return res; + } + + protected override List Trim(List ticks, Range range) + { + int startIndex = 0; + for (int i = 0; i < ticks.Count - 1; i++) + { + if (ticks[i] <= range.Min && range.Min <= ticks[i + 1]) + { + startIndex = i; + break; + } + } + + int endIndex = ticks.Count - 1; + for (int i = ticks.Count - 1; i >= 1; i--) + { + if (ticks[i] >= range.Max && range.Max > ticks[i - 1]) + { + endIndex = i; + break; + } + } + + List res = new List(endIndex - startIndex + 1); + for (int i = startIndex; i <= endIndex; i++) + { + res.Add(ticks[i]); + } + + return res; + } + + protected sealed override bool IsMinDate(TimeSpan dt) + { + return false; + } + } + + internal sealed class DayTimeSpanProvider : TimeSpanTicksProviderBase + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Day; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 20, 10, 5, 2, 1 }; + } + + protected override int GetSpecificValue(TimeSpan start, TimeSpan dt) + { + return (dt - start).Days; + } + + protected override TimeSpan GetStart(TimeSpan start, int value, int step) + { + double days = start.TotalDays; + double newDays = ((int)(days / step)) * step; + if (newDays > days) { + newDays -= step; + } + return TimeSpan.FromDays(newDays); + //return TimeSpan.FromDays(start.Days); + } + + protected override TimeSpan AddStep(TimeSpan dt, int step) + { + return dt.Add(TimeSpan.FromDays(step)); + } + } + + internal sealed class HourTimeSpanProvider : TimeSpanTicksProviderBase + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Hour; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 24, 12, 6, 4, 3, 2, 1 }; + } + + protected override int GetSpecificValue(TimeSpan start, TimeSpan dt) + { + return (int)(dt - start).TotalHours; + } + + protected override TimeSpan GetStart(TimeSpan start, int value, int step) + { + double hours = start.TotalHours; + double newHours = ((int)(hours / step)) * step; + if (newHours > hours) + { + newHours -= step; + } + return TimeSpan.FromHours(newHours); + //return TimeSpan.FromDays(start.Days); + } + + protected override TimeSpan AddStep(TimeSpan dt, int step) + { + return dt.Add(TimeSpan.FromHours(step)); + } + } + + internal sealed class MinuteTimeSpanProvider : TimeSpanTicksProviderBase + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Minute; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 60, 30, 20, 15, 10, 5, 4, 3, 2 }; + } + + protected override int GetSpecificValue(TimeSpan start, TimeSpan dt) + { + return (int)(dt - start).TotalMinutes; + } + + protected override TimeSpan GetStart(TimeSpan start, int value, int step) + { + double minutes = start.TotalMinutes; + double newMinutes = ((int)(minutes / step)) * step; + if (newMinutes > minutes) + { + newMinutes -= step; + } + + return TimeSpan.FromMinutes(newMinutes); + } + + protected override TimeSpan AddStep(TimeSpan dt, int step) + { + return dt.Add(TimeSpan.FromMinutes(step)); + } + } + + internal sealed class SecondTimeSpanProvider : TimeSpanTicksProviderBase + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Second; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 60, 30, 20, 15, 10, 5, 4, 3, 2 }; + } + + protected override int GetSpecificValue(TimeSpan start, TimeSpan dt) + { + return (int)(dt - start).TotalSeconds; + } + + protected override TimeSpan GetStart(TimeSpan start, int value, int step) + { + double seconds = start.TotalSeconds; + double newSeconds = ((int)(seconds / step)) * step; + if (newSeconds > seconds) { + newSeconds -= step; + } + + return TimeSpan.FromSeconds(newSeconds); + //return new TimeSpan(start.Days, start.Hours, start.Minutes, 0); + } + + protected override TimeSpan AddStep(TimeSpan dt, int step) + { + return dt.Add(TimeSpan.FromSeconds(step)); + } + } + + internal sealed class MillisecondTimeSpanProvider : TimeSpanTicksProviderBase + { + protected override DifferenceIn GetDifferenceCore() + { + return DifferenceIn.Millisecond; + } + + protected override int[] GetTickCountsCore() + { + return new int[] { 100, 50, 40, 25, 20, 10, 5, 4, 2 }; + } + + protected override int GetSpecificValue(TimeSpan start, TimeSpan dt) + { + return (int)(dt - start).TotalMilliseconds; + } + + protected override TimeSpan GetStart(TimeSpan start, int value, int step) + { + double millis = start.TotalMilliseconds; + double newMillis = ((int)(millis / step)) * step; + if (newMillis > millis) { + newMillis -= step; + } + + return TimeSpan.FromMilliseconds(newMillis); + //return start; + } + + protected override TimeSpan AddStep(TimeSpan dt, int step) + { + return dt.Add(TimeSpan.FromMilliseconds(step)); + } + } +} diff --git a/Charts/Axes/TimeSpan/TimeSpanToDoubleConversion.cs b/Charts/Axes/TimeSpan/TimeSpanToDoubleConversion.cs new file mode 100644 index 0000000..cda8f2a --- /dev/null +++ b/Charts/Axes/TimeSpan/TimeSpanToDoubleConversion.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal sealed class TimeSpanToDoubleConversion + { + public TimeSpanToDoubleConversion(TimeSpan minSpan, TimeSpan maxSpan) + : this(0, minSpan, 1, maxSpan) + { } + + public TimeSpanToDoubleConversion(double min, TimeSpan minSpan, double max, TimeSpan maxSpan) + { + this.min = min; + this.length = max - min; + this.ticksMin = minSpan.Ticks; + this.ticksLength = maxSpan.Ticks - ticksMin; + } + + private double min; + private double length; + private long ticksMin; + private long ticksLength; + + internal TimeSpan FromDouble(double d) + { + double ratio = (d - min) / length; + long ticks = (long)(ticksMin + ticksLength * ratio); + + ticks = MathHelper.Clamp(ticks, TimeSpan.MinValue.Ticks, TimeSpan.MaxValue.Ticks); + + return new TimeSpan(ticks); + } + + internal double ToDouble(TimeSpan span) + { + double ratio = (span.Ticks - ticksMin) / (double)ticksLength; + return min + ratio * length; + } + } + +} diff --git a/Charts/Axes/TimeSpan/TimeTicksProviderBase.cs b/Charts/Axes/TimeSpan/TimeTicksProviderBase.cs new file mode 100644 index 0000000..46836d5 --- /dev/null +++ b/Charts/Axes/TimeSpan/TimeTicksProviderBase.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public abstract class TimeTicksProviderBase : ITicksProvider + { + public event EventHandler Changed; + protected void RaiseChanged() + { + if (Changed != null) + { + Changed(this, EventArgs.Empty); + } + } + + private static readonly Dictionary> providers = + new Dictionary>(); + + protected static Dictionary> Providers + { + get { return TimeTicksProviderBase.providers; } + } + + private static readonly Dictionary> minorProviders = + new Dictionary>(); + + protected static Dictionary> MinorProviders + { + get { return TimeTicksProviderBase.minorProviders; } + } + + protected abstract TimeSpan GetDifference(T start, T end); + + #region ITicksProvider Members + + private IDateTimeTicksStrategy strategy = new DefaultDateTimeTicksStrategy(); + public IDateTimeTicksStrategy Strategy + { + get { return strategy; } + set + { + if (strategy != value) + { + strategy = value; + RaiseChanged(); + } + } + } + + private ITicksInfo result; + private DifferenceIn diff; + + public ITicksInfo GetTicks(Range range, int ticksCount) + { + Verify.IsTrue(ticksCount > 0); + + T start = range.Min; + T end = range.Max; + TimeSpan length = GetDifference(start, end); + + diff = strategy.GetDifference(length); + + TicksInfo result = new TicksInfo { Info = diff }; + if (providers.ContainsKey(diff)) + { + ITicksInfo innerResult = providers[diff].GetTicks(range, ticksCount); + T[] ticks = ModifyTicksGuard(innerResult.Ticks, diff); + + result.Ticks = ticks; + this.result = result; + return result; + } + + throw new InvalidOperationException(Strings.Exceptions.UnsupportedRangeInAxis); + } + + private T[] ModifyTicksGuard(T[] ticks, object info) + { + var result = ModifyTicks(ticks, info); + if (result == null) + throw new ArgumentNullException("ticks"); + + return result; + } + + protected virtual T[] ModifyTicks(T[] ticks, object info) + { + return ticks; + } + + /// + /// Decreases the tick count. + /// + /// The tick count. + /// + public int DecreaseTickCount(int ticksCount) + { + if (providers.ContainsKey(diff)) + return providers[diff].DecreaseTickCount(ticksCount); + + int res = ticksCount / 2; + if (res < 2) res = 2; + return res; + } + + /// + /// Increases the tick count. + /// + /// The tick count. + /// + public int IncreaseTickCount(int ticksCount) + { + DebugVerify.Is(ticksCount < 2000); + + if (providers.ContainsKey(diff)) + return providers[diff].IncreaseTickCount(ticksCount); + + return ticksCount * 2; + } + + public ITicksProvider MinorProvider + { + get + { + DifferenceIn smallerDiff = DifferenceIn.Smallest; + if (strategy.TryGetLowerDiff(diff, out smallerDiff) && minorProviders.ContainsKey(smallerDiff)) + { + var minorProvider = (MinorTimeProviderBase)minorProviders[smallerDiff]; + minorProvider.SetTicks(result.Ticks); + return minorProvider; + } + + return null; + // todo What to do if this already is the smallest provider? + } + } + + public ITicksProvider MajorProvider + { + get + { + DifferenceIn biggerDiff = DifferenceIn.Smallest; + if (strategy.TryGetBiggerDiff(diff, out biggerDiff)) + { + return providers[biggerDiff]; + } + + return null; + // todo What to do if this already is the biggest provider? + } + } + + #endregion + } +} diff --git a/Charts/Axes/TimeSpan/VerticalTimeSpanAxis.cs b/Charts/Axes/TimeSpan/VerticalTimeSpanAxis.cs new file mode 100644 index 0000000..5ba3d8a --- /dev/null +++ b/Charts/Axes/TimeSpan/VerticalTimeSpanAxis.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a vertical axis with values of type. + /// + public class VerticalTimeSpanAxis : TimeSpanAxis + { + /// + /// Initializes a new instance of the class, placed (by default) on the left side of . + /// + public VerticalTimeSpanAxis() + { + Placement = AxisPlacement.Left; + } + + /// + /// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc. + /// If proposed placement is wrong, throws an ArgumentException. + /// + /// The new placement. + protected override void ValidatePlacement(AxisPlacement newPlacement) + { + if (newPlacement == AxisPlacement.Bottom || newPlacement == AxisPlacement.Top) + throw new ArgumentException(Strings.Exceptions.VerticalAxisCannotBeHorizontal); + } + } +} diff --git a/Charts/BackgroundRenderer.cs b/Charts/BackgroundRenderer.cs new file mode 100644 index 0000000..a05049b --- /dev/null +++ b/Charts/BackgroundRenderer.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public static class BackgroundRenderer + { + public static readonly RoutedEvent RenderingFinished = EventManager.RegisterRoutedEvent( + "RenderingFinished", + RoutingStrategy.Bubble, + typeof(RoutedEventHandler), + typeof(BackgroundRenderer)); + + public static void RaiseRenderingFinished(FrameworkElement eventSource) + { + eventSource.RaiseEvent(new RoutedEventArgs(RenderingFinished)); + } + + public static readonly RoutedEvent UpdateRequested = EventManager.RegisterRoutedEvent( + "UpdateRequested", + RoutingStrategy.Bubble, + typeof(RoutedEventHandler), + typeof(BackgroundRenderer)); + + public static void RaiseUpdateRequested(FrameworkElement eventSource) + { + eventSource.RaiseEvent(new RoutedEventArgs(UpdateRequested)); + } + + #region UsesBackgroundRendering + + public static bool GetUsesBackgroundRendering(DependencyObject obj) + { + return (bool)obj.GetValue(UsesBackgroundRenderingProperty); + } + + public static void SetUsesBackgroundRendering(DependencyObject obj, bool value) + { + obj.SetValue(UsesBackgroundRenderingProperty, value); + } + + public static readonly DependencyProperty UsesBackgroundRenderingProperty = DependencyProperty.RegisterAttached( + "UsesBackgroundRendering", + typeof(bool), + typeof(BackgroundRenderer), + new FrameworkPropertyMetadata(false)); + + #endregion // end of UsesBackgroundRendering + } +} diff --git a/Charts/BitmapBasedGraph.cs b/Charts/BitmapBasedGraph.cs new file mode 100644 index 0000000..1bab1b1 --- /dev/null +++ b/Charts/BitmapBasedGraph.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +//using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using Microsoft.Research.DynamicDataDisplay.Charts; +using Microsoft.Research.DynamicDataDisplay.Common; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.DataSources; +using Microsoft.Research.DynamicDataDisplay.PointMarkers; +using Microsoft.Research.DynamicDataDisplay.Charts.Shapes; +using System.Windows.Input; + +namespace Microsoft.Research.DynamicDataDisplay +{ + public abstract class BitmapBasedGraph : ViewportElement2D + { + static BitmapBasedGraph() + { + Type thisType = typeof(BitmapBasedGraph); + BackgroundRenderer.UsesBackgroundRenderingProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(true)); + } + + private int nextRequestId = 0; + + /// Latest complete request + private RenderRequest completedRequest = null; + + /// Currently running request + private RenderRequest activeRequest = null; + + /// Result of latest complete request + private BitmapSource completedBitmap = null; + + /// Pending render request + private RenderRequest pendingRequest = null; + + /// Single apartment thread used for background rendering + /// STA is required for creating WPF components in this thread + private Thread renderThread = null; + + private AutoResetEvent renderRequested = new AutoResetEvent(false); + + private ManualResetEvent shutdownRequested = new ManualResetEvent(false); + + /// True means that current bitmap is invalidated and is to be re-rendered. + private bool bitmapInvalidated = true; + + /// Shows tooltips. + private PopupTip popup; + + /// + /// Initializes a new instance of the class. + /// + public BitmapBasedGraph() + { + ManualTranslate = true; + } + + protected virtual UIElement GetTooltipForPoint(Point point, DataRect visible, Rect output) + { + return null; + } + + protected PopupTip GetPopupTipWindow() + { + if (popup != null) + return popup; + + foreach (var item in Plotter.Children) + { + if (item is ViewportUIContainer) + { + ViewportUIContainer container = (ViewportUIContainer)item; + if (container.Content is PopupTip) + return popup = (PopupTip)container.Content; + } + } + + popup = new PopupTip(); + popup.Placement = PlacementMode.Relative; + popup.PlacementTarget = this; + Plotter.Children.Add(popup); + return popup; + } + + protected override void OnMouseMove(System.Windows.Input.MouseEventArgs e) + { + base.OnMouseMove(e); + + var popup = GetPopupTipWindow(); + if (popup.IsOpen) + popup.Hide(); + + if (bitmapInvalidated) return; + + Point p = e.GetPosition(this); + Point dp = p.ScreenToData(Plotter2D.Transform); + + var tooltip = GetTooltipForPoint(p, completedRequest.Visible, completedRequest.Output); + if (tooltip == null) return; + + popup.VerticalOffset = p.Y + 20; + popup.HorizontalOffset = p.X; + + popup.ShowDelayed(new TimeSpan(0, 0, 1)); + + Grid grid = new Grid(); + + Rectangle rect = new Rectangle + { + Stroke = Brushes.Black, + Fill = SystemColors.InfoBrush + }; + + StackPanel sp = new StackPanel(); + sp.Orientation = Orientation.Vertical; + sp.Children.Add(tooltip); + sp.Margin = new Thickness(4, 2, 4, 2); + + var tb = new TextBlock(); + tb.Text = String.Format("Location: {0:F2}, {1:F2}", dp.X, dp.Y); + tb.Foreground = SystemColors.GrayTextBrush; + sp.Children.Add(tb); + + grid.Children.Add(rect); + grid.Children.Add(sp); + grid.Measure(SizeHelper.CreateInfiniteSize()); + popup.Child = grid; + } + + protected override void OnMouseLeave(MouseEventArgs e) + { + base.OnMouseLeave(e); + GetPopupTipWindow().Hide(); + } + + /// + /// Adds a render task and invalidates visual. + /// + public void UpdateVisualization() + { + if (Viewport == null) return; + + Rect output = new Rect(this.RenderSize); + CreateRenderTask(Viewport.Visible, output); + InvalidateVisual(); + } + + protected override void OnVisibleChanged(DataRect newRect, DataRect oldRect) + { + base.OnVisibleChanged(newRect, oldRect); + CreateRenderTask(newRect, Viewport.Output); + InvalidateVisual(); + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + base.OnRenderSizeChanged(sizeInfo); + CreateRenderTask(Viewport.Visible, new Rect(sizeInfo.NewSize)); + InvalidateVisual(); + } + + protected abstract BitmapSource RenderFrame(DataRect visible, Rect output); + + private void RenderThreadFunc() + { + WaitHandle[] events = new WaitHandle[] { renderRequested, shutdownRequested }; + while (true) + { + lock (this) + { + activeRequest = null; + if (pendingRequest != null) + { + activeRequest = pendingRequest; + pendingRequest = null; + } + } + if (activeRequest == null) + { + WaitHandle.WaitAny(events); + if (shutdownRequested.WaitOne(0)) + break; + } + else + { + try + { + BitmapSource result = (BitmapSource)RenderFrame(activeRequest.Visible, activeRequest.Output); + if (result != null) + Dispatcher.BeginInvoke( + new RenderCompletionHandler(OnRenderCompleted), + new RenderResult(activeRequest, result)); + } + catch (Exception exc) + { + Trace.WriteLine(String.Format("RenderRequest {0} failed: {1}", activeRequest.RequestID, exc.Message)); + } + } + } + } + + private void CreateRenderTask(DataRect visible, Rect output) + { + lock (this) + { + bitmapInvalidated = true; + + if (activeRequest != null) + activeRequest.Cancel(); + pendingRequest = new RenderRequest(nextRequestId++, visible, output); + renderRequested.Set(); + } + if (renderThread == null) + { + renderThread = new Thread(RenderThreadFunc); + renderThread.IsBackground = true; + renderThread.SetApartmentState(ApartmentState.STA); + renderThread.Start(); + } + } + + private delegate void RenderCompletionHandler(RenderResult result); + + protected virtual void OnRenderCompleted(RenderResult result) + { + if (result.IsSuccess) + { + completedRequest = result.Request; + completedBitmap = result.CreateBitmap(); + bitmapInvalidated = false; + + InvalidateVisual(); + BackgroundRenderer.RaiseRenderingFinished(this); + } + } + + protected override void OnRenderCore(DrawingContext dc, RenderState state) + { + if (completedRequest != null && completedBitmap != null) + dc.DrawImage(completedBitmap, completedRequest.Visible.ViewportToScreen(Viewport.Transform)); + } + } + + public class RenderRequest + { + private int requestId; + private DataRect visible; + private Rect output; + private int cancelling; + + public RenderRequest(int requestId, DataRect visible, Rect output) + { + this.requestId = requestId; + this.visible = visible; + this.output = output; + } + + public int RequestID + { + get { return requestId; } + } + + public DataRect Visible + { + get { return visible; } + } + + public Rect Output + { + get { return output; } + } + + public bool IsCancellingRequested + { + get { return cancelling > 0; } + } + + public void Cancel() + { + Interlocked.Increment(ref cancelling); + } + } + + public class RenderResult + { + private RenderRequest request; + private int pixelWidth, pixelHeight, stride; + private double dpiX, dpiY; + private BitmapPalette palette; + private PixelFormat format; + private Array pixels; + + /// Constructs successul rendering result + /// Source request + /// Rendered bitmap + public RenderResult(RenderRequest request, BitmapSource result) + { + this.request = request; + pixelWidth = result.PixelWidth; + pixelHeight = result.PixelHeight; + stride = result.PixelWidth * result.Format.BitsPerPixel / 8; + dpiX = result.DpiX; + dpiY = result.DpiY; + palette = result.Palette; + format = result.Format; + pixels = new byte[pixelHeight * stride]; + result.CopyPixels(pixels, stride, 0); + } + + public RenderRequest Request + { + get + { + return request; + } + } + + public bool IsSuccess + { + get + { + return pixels != null; + } + } + + public BitmapSource CreateBitmap() + { + return BitmapFrame.Create(pixelWidth, pixelHeight, dpiX, dpiY, + format, palette, pixels, stride); + } + } +} diff --git a/Charts/ContentGraph.cs b/Charts/ContentGraph.cs new file mode 100644 index 0000000..07bc266 --- /dev/null +++ b/Charts/ContentGraph.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Common; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public abstract class ContentGraph : ContentControl, IPlotterElement + { + static ContentGraph() + { + EventManager.RegisterClassHandler(typeof(ContentGraph), Plotter.PlotterChangedEvent, new PlotterChangedEventHandler(OnPlotterChanged)); + } + + private static void OnPlotterChanged(object sender, PlotterChangedEventArgs e) + { + ContentGraph owner = (ContentGraph)sender; + owner.OnPlotterChanged(e); + } + + private void OnPlotterChanged(PlotterChangedEventArgs e) + { + if (plotter == null && e.CurrentPlotter != null) + { + plotter = (Plotter2D)e.CurrentPlotter; + plotter.Viewport.PropertyChanged += Viewport_PropertyChanged; + OnPlotterAttached(); + } + if (plotter != null && e.PreviousPlotter != null) + { + OnPlotterDetaching(); + plotter.Viewport.PropertyChanged -= Viewport_PropertyChanged; + plotter = null; + } + } + + #region IPlotterElement Members + + private void Viewport_PropertyChanged(object sender, ExtendedPropertyChangedEventArgs e) + { + OnViewportPropertyChanged(e); + } + + protected virtual void OnViewportPropertyChanged(ExtendedPropertyChangedEventArgs e) { } + + protected virtual Panel HostPanel + { + get { return plotter.CentralGrid; } + } + + void IPlotterElement.OnPlotterAttached(Plotter plotter) + { + this.plotter = (Plotter2D)plotter; + HostPanel.Children.Add(this); + this.plotter.Viewport.PropertyChanged += Viewport_PropertyChanged; + + OnPlotterAttached(); + } + + protected virtual void OnPlotterAttached() { } + + void IPlotterElement.OnPlotterDetaching(Plotter plotter) + { + OnPlotterDetaching(); + + this.plotter.Viewport.PropertyChanged -= Viewport_PropertyChanged; + HostPanel.Children.Remove(this); + this.plotter = null; + } + + protected virtual void OnPlotterDetaching() { } + + private Plotter2D plotter; + protected Plotter2D Plotter2D + { + get { return plotter; } + } + + Plotter IPlotterElement.Plotter + { + get { return plotter; } + } + + #endregion + } +} diff --git a/Charts/DataFollowChart.cs b/Charts/DataFollowChart.cs new file mode 100644 index 0000000..30aee80 --- /dev/null +++ b/Charts/DataFollowChart.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Shapes; +using System.Windows.Media; +using System.Windows.Input; +using Microsoft.Research.DynamicDataDisplay.Common.DataSearch; +using System.Diagnostics; +using System.Windows.Markup; +using System.ComponentModel; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a marker with position.X bound to mouse cursor's position and position.Y is determined by interpolation of 's points. + /// + [ContentProperty("MarkerTemplate")] + public class DataFollowChart : ViewportHostPanel, INotifyPropertyChanged + { + /// + /// Initializes a new instance of the class. + /// + public DataFollowChart() + { + Marker = CreateDefaultMarker(); + SetX(marker, 0); + SetY(marker, 0); + Children.Add(marker); + } + + private static Ellipse CreateDefaultMarker() + { + return new Ellipse + { + Width = 10, + Height = 10, + Stroke = Brushes.Green, + StrokeThickness = 1, + Fill = Brushes.LightGreen, + Visibility = Visibility.Hidden + }; + } + + /// + /// Initializes a new instance of the class, bound to specified . + /// + /// The point source. + public DataFollowChart(PointsGraphBase pointSource) + : this() + { + PointSource = pointSource; + } + + #region MarkerTemplate property + + /// + /// Gets or sets the template, used to create a marker. This is a dependency property. + /// + /// The marker template. + public DataTemplate MarkerTemplate + { + get { return (DataTemplate)GetValue(MarkerTemplateProperty); } + set { SetValue(MarkerTemplateProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty MarkerTemplateProperty = DependencyProperty.Register( + "MarkerTemplate", + typeof(DataTemplate), + typeof(DataFollowChart), + new FrameworkPropertyMetadata(null, OnMarkerTemplateChanged)); + + private static void OnMarkerTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataFollowChart chart = (DataFollowChart)d; + DataTemplate template = (DataTemplate)e.NewValue; + + FrameworkElement marker; + if (template != null) + { + marker = (FrameworkElement)template.LoadContent(); + } + else + { + marker = CreateDefaultMarker(); + } + + chart.Children.Remove(chart.marker); + chart.Marker = marker; + chart.Children.Add(marker); + } + + #endregion + + #region Point sources + + /// + /// Gets or sets the source of points. + /// Can be null. + /// + /// The point source. + public PointsGraphBase PointSource + { + get { return (PointsGraphBase)GetValue(PointSourceProperty); } + set { SetValue(PointSourceProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty PointSourceProperty = DependencyProperty.Register( + "PointSource", + typeof(PointsGraphBase), + typeof(DataFollowChart), + new FrameworkPropertyMetadata(null, OnPointSourceChanged)); + + private static void OnPointSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataFollowChart chart = (DataFollowChart)d; + + PointsGraphBase previous = e.OldValue as PointsGraphBase; + if (previous != null) + { + previous.VisiblePointsChanged -= chart.Source_VisiblePointsChanged; + } + + PointsGraphBase current = e.NewValue as PointsGraphBase; + if (current != null) + { + current.ProvideVisiblePoints = true; + current.VisiblePointsChanged += chart.Source_VisiblePointsChanged; + if (current.VisiblePoints != null) + { + chart.searcher = new SortedXSearcher1d(current.VisiblePoints); + } + } + + chart.UpdateUIRepresentation(); + } + + private SearchResult1d searchResult = SearchResult1d.Empty; + private SortedXSearcher1d searcher; + private FrameworkElement marker; + + [NotNull] + public FrameworkElement Marker + { + get { return marker; } + protected set + { + marker = value; + marker.DataContext = followDataContext; + PropertyChanged.Raise(this, "Marker"); + } + } + + private FollowDataContext followDataContext = new FollowDataContext(); + public FollowDataContext FollowDataContext + { + get { return followDataContext; } + } + + private void UpdateUIRepresentation() + { + if (Plotter == null) + return; + + PointsGraphBase source = this.PointSource; + + if (source == null || (source != null && source.VisiblePoints == null)) + { + SetValue(MarkerPositionPropertyKey, new Point(Double.NaN, Double.NaN)); + marker.Visibility = Visibility.Hidden; + return; + } + else + { + Point mousePos = Mouse.GetPosition(Plotter.CentralGrid); + + var transform = Plotter.Transform; + Point viewportPos = mousePos.ScreenToViewport(transform); + + double x = viewportPos.X; + searchResult = searcher.SearchXBetween(x, searchResult); + SetValue(ClosestPointIndexPropertyKey, searchResult.Index); + if (!searchResult.IsEmpty) + { + marker.Visibility = Visibility.Visible; + + IList points = source.VisiblePoints; + Point ptBefore = points[searchResult.Index]; + Point ptAfter = points[searchResult.Index + 1]; + + double ratio = (x - ptBefore.X) / (ptAfter.X - ptBefore.X); + double y = ptBefore.Y + (ptAfter.Y - ptBefore.Y) * ratio; + + Point temp = new Point(x, y); + SetX(marker, temp.X); + SetY(marker, temp.Y); + + Point markerPosition = temp; + followDataContext.Position = markerPosition; + SetValue(MarkerPositionPropertyKey, markerPosition); + } + else + { + SetValue(MarkerPositionPropertyKey, new Point(Double.NaN, Double.NaN)); + marker.Visibility = Visibility.Hidden; + } + } + } + + #region ClosestPointIndex property + + private static readonly DependencyPropertyKey ClosestPointIndexPropertyKey = DependencyProperty.RegisterReadOnly( + "ClosestPointIndex", + typeof(int), + typeof(DataFollowChart), + new PropertyMetadata(-1) + ); + + public static readonly DependencyProperty ClosestPointIndexProperty = ClosestPointIndexPropertyKey.DependencyProperty; + + public int ClosestPointIndex + { + get { return (int)GetValue(ClosestPointIndexProperty); } + } + + #endregion + + #region MarkerPositionProperty + + private static readonly DependencyPropertyKey MarkerPositionPropertyKey = DependencyProperty.RegisterReadOnly( + "MarkerPosition", + typeof(Point), + typeof(DataFollowChart), + new PropertyMetadata(new Point(), OnMarkerPositionChanged)); + + private static void OnMarkerPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataFollowChart chart = (DataFollowChart)d; + chart.MarkerPositionChanged.Raise(chart); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty MarkerPositionProperty = MarkerPositionPropertyKey.DependencyProperty; + + /// + /// Gets the marker position. + /// + /// The marker position. + public Point MarkerPosition + { + get { return (Point)GetValue(MarkerPositionProperty); } + } + + /// + /// Occurs when marker position changes. + /// + public event EventHandler MarkerPositionChanged; + + #endregion + + public override void OnPlotterAttached(Plotter plotter) + { + base.OnPlotterAttached(plotter); + plotter.MainGrid.MouseMove += MainGrid_MouseMove; + } + + private void MainGrid_MouseMove(object sender, MouseEventArgs e) + { + UpdateUIRepresentation(); + } + + public override void OnPlotterDetaching(Plotter plotter) + { + plotter.MainGrid.MouseMove -= MainGrid_MouseMove; + base.OnPlotterDetaching(plotter); + } + + protected override void Viewport_PropertyChanged(object sender, ExtendedPropertyChangedEventArgs e) + { + base.Viewport_PropertyChanged(sender, e); + UpdateUIRepresentation(); + } + + private void Source_VisiblePointsChanged(object sender, EventArgs e) + { + PointsGraphBase source = (PointsGraphBase)sender; + if (source.VisiblePoints != null) + { + searcher = new SortedXSearcher1d(source.VisiblePoints); + } + UpdateUIRepresentation(); + } + + #endregion + + #region INotifyPropertyChanged Members + + /// + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + } + + /// + /// Represents a special data context, which encapsulates marker's position and custom data. + /// Used in . + /// + public class FollowDataContext : INotifyPropertyChanged + { + private Point position; + /// + /// Gets or sets the position of marker. + /// + /// The position. + public Point Position + { + get { return position; } + set + { + position = value; + PropertyChanged.Raise(this, "Position"); + } + } + + private object data; + /// + /// Gets or sets the additional custom data. + /// + /// The data. + public object Data + { + get { return data; } + set + { + data = value; + PropertyChanged.Raise(this, "Data"); + } + } + + #region INotifyPropertyChanged Members + + /// + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + } +} diff --git a/Charts/DataSource2dContext.cs b/Charts/DataSource2dContext.cs new file mode 100644 index 0000000..8a8cba6 --- /dev/null +++ b/Charts/DataSource2dContext.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + // todo probably remove + public sealed class DataSource2dContext : DependencyObject + { + public static DataRect GetVisibleRect(DependencyObject obj) + { + return (DataRect)obj.GetValue(VisibleRectProperty); + } + + public static void SetVisibleRect(DependencyObject obj, DataRect value) + { + obj.SetValue(VisibleRectProperty, value); + } + + public static readonly DependencyProperty VisibleRectProperty = DependencyProperty.RegisterAttached( + "VisibleRect", + typeof(DataRect), + typeof(DataSource2dContext), + new FrameworkPropertyMetadata(new DataRect())); + + public static Rect GetScreenRect(DependencyObject obj) + { + return (Rect)obj.GetValue(ScreenRectProperty); + } + + public static void SetScreenRect(DependencyObject obj, Rect value) + { + obj.SetValue(ScreenRectProperty, value); + } + + public static readonly DependencyProperty ScreenRectProperty = DependencyProperty.RegisterAttached( + "ScreenRect", + typeof(Rect), + typeof(DataSource2dContext), + new FrameworkPropertyMetadata(new Rect())); + } +} + diff --git a/Charts/DebugMenu.cs b/Charts/DebugMenu.cs new file mode 100644 index 0000000..2a32647 --- /dev/null +++ b/Charts/DebugMenu.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a menu that appears in Debug version of DynamicDataDisplay. + /// + public class DebugMenu : IPlotterElement + { + /// + /// Initializes a new instance of the class. + /// + public DebugMenu() + { + Panel.SetZIndex(menu, 1); + } + + private Plotter plotter; + private readonly Menu menu = new Menu + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(3) + }; + public Menu Menu + { + get { return menu; } + } + + public MenuItem TryFindMenuItem(string itemName) + { + return menu.Items.OfType().Where(item => item.Header.Equals(itemName)).FirstOrDefault(); + } + + #region IPlotterElement Members + + public void OnPlotterAttached(Plotter plotter) + { + this.plotter = plotter; + plotter.CentralGrid.Children.Add(menu); + } + + public void OnPlotterDetaching(Plotter plotter) + { + plotter.CentralGrid.Children.Remove(menu); + this.plotter = null; + } + + public Plotter Plotter + { + get { return plotter; } + } + + #endregion + } +} diff --git a/Charts/FakePointList.cs b/Charts/FakePointList.cs new file mode 100644 index 0000000..b143a83 --- /dev/null +++ b/Charts/FakePointList.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay +{ + [DebuggerDisplay("Count = {Count}")] + public sealed class FakePointList : IList { + private int first; + private int last; + private int count; + private Point startPoint; + private bool hasPoints; + + private double leftBound; + private double rightBound; + private readonly List points; + + internal FakePointList(List points, double left, double right) { + this.points = points; + this.leftBound = left; + this.rightBound = right; + + Calc(); + } + + internal void SetXBorders(double left, double right) { + this.leftBound = left; + this.rightBound = right; + Calc(); + } + + private void Calc() { + Debug.Assert(leftBound <= rightBound); + + first = points.FindIndex(p => p.X > leftBound); + if (first > 0) + first--; + + last = points.FindLastIndex(p => p.X < rightBound); + + if (last < points.Count - 1) + last++; + count = last - first; + hasPoints = first >= 0 && last > 0; + + if (hasPoints) { + startPoint = points[first]; + } + } + + public Point StartPoint { + get { return startPoint; } + } + + public bool HasPoints { + get { return hasPoints; } + } + + #region IList Members + + public int IndexOf(Point item) { + throw new NotSupportedException(); + } + + public void Insert(int index, Point item) { + throw new NotSupportedException(); + } + + public void RemoveAt(int index) { + throw new NotSupportedException(); + } + + public Point this[int index] { + get { + return points[first + 1 + index]; + } + set { + throw new NotSupportedException(); + } + } + + #endregion + + #region ICollection Members + + public void Add(Point item) { + throw new NotSupportedException(); + } + + public void Clear() { + throw new NotSupportedException(); + } + + public bool Contains(Point item) { + throw new NotSupportedException(); + } + + public void CopyTo(Point[] array, int arrayIndex) { + throw new NotSupportedException(); + } + + public int Count { + get { return count; } + } + + public bool IsReadOnly { + get { throw new NotSupportedException(); } + } + + public bool Remove(Point item) { + throw new NotSupportedException(); + } + + #endregion + + #region IEnumerable Members + + public IEnumerator GetEnumerator() { + for (int i = first + 1; i <= last; i++) { + yield return points[i]; + } + } + + #endregion + + #region IEnumerable Members + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/Charts/FilterCollection.cs b/Charts/FilterCollection.cs new file mode 100644 index 0000000..17ded1c --- /dev/null +++ b/Charts/FilterCollection.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Filters; +using Microsoft.Research.DynamicDataDisplay.Common; +using System.Collections.Specialized; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Represents a collection of point filters of . + /// + public sealed class FilterCollection : D3Collection + { + protected override void OnItemAdding(IPointsFilter item) + { + if (item == null) + throw new ArgumentNullException("item"); + } + + protected override void OnItemAdded(IPointsFilter item) + { + item.Changed += OnItemChanged; + } + + private void OnItemChanged(object sender, EventArgs e) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + protected override void OnItemRemoving(IPointsFilter item) + { + item.Changed -= OnItemChanged; + } + + internal List Filter(List points, Rect screenRect) + { + foreach (var filter in Items) + { + filter.SetScreenRect(screenRect); + points = filter.Filter(points); + } + + return points; + } + } +} diff --git a/Charts/Filters/EmptyFilter.cs b/Charts/Filters/EmptyFilter.cs new file mode 100644 index 0000000..7ac3d9f --- /dev/null +++ b/Charts/Filters/EmptyFilter.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Filters +{ + public sealed class EmptyFilter : PointsFilterBase + { + public override List Filter(List points) + { + return points; + } + } +} diff --git a/Charts/Filters/FrequencyFilter.cs b/Charts/Filters/FrequencyFilter.cs new file mode 100644 index 0000000..68a5e23 --- /dev/null +++ b/Charts/Filters/FrequencyFilter.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Charts.Filters; + +namespace Microsoft.Research.DynamicDataDisplay.Filters +{ + public sealed class FrequencyFilter : PointsFilterBase + { + + /// Visible region in screen coordinates + private Rect screenRect; + + #region IPointFilter Members + + public override void SetScreenRect(Rect screenRect) + { + this.screenRect = screenRect; + } + + // todo probably use LINQ here. + public override List Filter(List points) + { + if (points.Count == 0) return points; + + List resultPoints = points; + List currentChain = new List(); + + if (points.Count > 2 * screenRect.Width) + { + resultPoints = new List(); + + double currentX = Math.Floor(points[0].X); + foreach (Point p in points) + { + if (Math.Floor(p.X) == currentX) + { + currentChain.Add(p); + } + else + { + // Analyse current chain + if (currentChain.Count <= 2) + { + resultPoints.AddRange(currentChain); + } + else + { + Point first = MinByX(currentChain); + Point last = MaxByX(currentChain); + Point min = MinByY(currentChain); + Point max = MaxByY(currentChain); + resultPoints.Add(first); + + Point smaller = min.X < max.X ? min : max; + Point greater = min.X > max.X ? min : max; + if (smaller != resultPoints.GetLast()) + { + resultPoints.Add(smaller); + } + if (greater != resultPoints.GetLast()) + { + resultPoints.Add(greater); + } + if (last != resultPoints.GetLast()) + { + resultPoints.Add(last); + } + } + currentChain.Clear(); + currentChain.Add(p); + currentX = Math.Floor(p.X); + } + } + } + + resultPoints.AddRange(currentChain); + + return resultPoints; + } + + #endregion + + private static Point MinByX(IList points) + { + Point minPoint = points[0]; + foreach (Point p in points) + { + if (p.X < minPoint.X) + { + minPoint = p; + } + } + return minPoint; + } + + private static Point MaxByX(IList points) + { + Point maxPoint = points[0]; + foreach (Point p in points) + { + if (p.X > maxPoint.X) + { + maxPoint = p; + } + } + return maxPoint; + } + + private static Point MinByY(IList points) + { + Point minPoint = points[0]; + foreach (Point p in points) + { + if (p.Y < minPoint.Y) + { + minPoint = p; + } + } + return minPoint; + } + + private static Point MaxByY(IList points) + { + Point maxPoint = points[0]; + foreach (Point p in points) + { + if (p.Y > maxPoint.Y) + { + maxPoint = p; + } + } + return maxPoint; + } + } +} diff --git a/Charts/Filters/FrequencyFilter2.cs b/Charts/Filters/FrequencyFilter2.cs new file mode 100644 index 0000000..586878d --- /dev/null +++ b/Charts/Filters/FrequencyFilter2.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Charts.Filters; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Filters +{ + public class FrequencyFilter2 : PointsFilterBase + { + private Rect screenRect; + public override void SetScreenRect(Rect screenRect) + { + this.screenRect = screenRect; + } + + public override List Filter(List points) + { + List result = new List(); + + using (var enumerator = points.GetEnumerator()) + { + double currentX = Double.NegativeInfinity; + + double minX = 0, maxX = 0, minY = 0, maxY = 0; + + Point left = new Point(), right = new Point(), top = new Point(), bottom = new Point(); + + bool isFirstPoint = true; + while (enumerator.MoveNext()) + { + Point currPoint = enumerator.Current; + double x = currPoint.X; + double y = currPoint.Y; + double xInt = Math.Floor(x); + if (xInt == currentX) + { + if (x > maxX) + { + maxX = x; + right = currPoint; + } + + if (y > maxY) + { + maxY = y; + top = currPoint; + } + else if (y < minY) + { + minY = y; + bottom = currPoint; + } + } + else + { + if (!isFirstPoint) + { + result.Add(left); + + Point leftY = top.X < bottom.X ? top : bottom; + Point rightY = top.X > bottom.X ? top : bottom; + + if (top != bottom) + { + result.Add(leftY); + result.Add(rightY); + } + else if (top != left) + result.Add(top); + + if (right != rightY) + result.Add(right); + } + + currentX = xInt; + left = right = top = bottom = currPoint; + minX = maxX = x; + minY = maxY = y; + } + + isFirstPoint = false; + } + } + + return result; + } + } +} diff --git a/Charts/Filters/IPointsFilter.cs b/Charts/Filters/IPointsFilter.cs new file mode 100644 index 0000000..d3800b9 --- /dev/null +++ b/Charts/Filters/IPointsFilter.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Windows; +using System; + +namespace Microsoft.Research.DynamicDataDisplay.Filters +{ + /// Provides algorithm for filtering point lists in screen coordinates + public interface IPointsFilter + { + + /// Performs filtering + /// List of source points + /// List of filtered points + List Filter(List points); + + /// Sets visible rectangle in screen coordinates + /// Screen rectangle + /// Should be invoked before first call to + void SetScreenRect(Rect screenRect); + + event EventHandler Changed; + } +} diff --git a/Charts/Filters/InclinationFilter.cs b/Charts/Filters/InclinationFilter.cs new file mode 100644 index 0000000..512f127 --- /dev/null +++ b/Charts/Filters/InclinationFilter.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Charts.Filters; + +namespace Microsoft.Research.DynamicDataDisplay.Filters +{ + [Obsolete("Works incorrectly", true)] + public sealed class InclinationFilter : PointsFilterBase + { + private double criticalAngle = 179; + public double CriticalAngle + { + get { return criticalAngle; } + set + { + if (criticalAngle != value) + { + criticalAngle = value; + RaiseChanged(); + } + } + } + + #region IPointFilter Members + + public override List Filter(List points) + { + if (points.Count == 0) + return points; + + List res = new List { points[0] }; + + int i = 1; + while (i < points.Count) + { + bool added = false; + int j = i; + while (!added && (j < points.Count - 1)) + { + Point x1 = res[res.Count - 1]; + Point x2 = points[j]; + Point x3 = points[j + 1]; + + double a = (x1 - x2).Length; + double b = (x2 - x3).Length; + double c = (x1 - x3).Length; + + double angle13 = Math.Acos((a * a + b * b - c * c) / (2 * a * b)); + double degrees = 180 / Math.PI * angle13; + if (degrees < criticalAngle) + { + res.Add(x2); + added = true; + i = j + 1; + } + else + { + j++; + } + } + // reached the end of resultPoints + if (!added) + { + res.Add(points.GetLast()); + break; + } + } + return res; + } + + #endregion + } +} diff --git a/Charts/Filters/PointsFilterBase.cs b/Charts/Filters/PointsFilterBase.cs new file mode 100644 index 0000000..810d6a6 --- /dev/null +++ b/Charts/Filters/PointsFilterBase.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Filters; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Filters +{ + public abstract class PointsFilterBase : IPointsFilter + { + #region IPointsFilter Members + + public abstract List Filter(List points); + + public virtual void SetScreenRect(Rect screenRect) { } + + protected void RaiseChanged() + { + Changed.Raise(this); + } + public event EventHandler Changed; + + #endregion + } +} diff --git a/Charts/IOneDimensionalChart.cs b/Charts/IOneDimensionalChart.cs new file mode 100644 index 0000000..4c6f983 --- /dev/null +++ b/Charts/IOneDimensionalChart.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.DataSources; + +namespace Microsoft.Research.DynamicDataDisplay +{ + public interface IOneDimensionalChart + { + IPointDataSource DataSource { get; set; } + event EventHandler DataChanged; + } +} diff --git a/Charts/Isolines/AdditionalLinesRenderer.cs b/Charts/Isolines/AdditionalLinesRenderer.cs new file mode 100644 index 0000000..0adce9d --- /dev/null +++ b/Charts/Isolines/AdditionalLinesRenderer.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Media; +using System.Windows; +using System.Windows.Data; +using System.Diagnostics; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + public class AdditionalLinesRenderer : IsolineRenderer + { + protected override void CreateUIRepresentation() + { + InvalidateVisual(); + } + + protected override void OnPlotterAttached() + { + base.OnPlotterAttached(); + + FrameworkElement parent = (FrameworkElement)Parent; + var renderer = (FrameworkElement)parent.FindName("PART_IsolineRenderer"); + + Binding contentBoundsBinding = new Binding { Path = new PropertyPath("(0)", Viewport2D.ContentBoundsProperty), Source = renderer }; + SetBinding(Viewport2D.ContentBoundsProperty, contentBoundsBinding); + SetBinding(ViewportPanel.ViewportBoundsProperty, contentBoundsBinding); + + Plotter2D.Viewport.EndPanning += Viewport_EndPanning; + Plotter2D.Viewport.PropertyChanged += Viewport_PropertyChanged; + } + + void Viewport_PropertyChanged(object sender, ExtendedPropertyChangedEventArgs e) + { + if (e.PropertyName == "Visible") + { + if (Plotter2D.Viewport.PanningState == Viewport2DPanningState.NotPanning) + InvalidateVisual(); + } + } + + protected override void OnPlotterDetaching() + { + Plotter2D.Viewport.EndPanning -= Viewport_EndPanning; + Plotter2D.Viewport.PropertyChanged -= Viewport_PropertyChanged; + + base.OnPlotterDetaching(); + } + + private void Viewport_EndPanning(object sender, EventArgs e) + { + InvalidateVisual(); + } + + protected override void OnRender(DrawingContext drawingContext) + { + if (Plotter2D == null) return; + if (DataSource == null) return; + + var collection = (IsolineCollection)Parent.GetValue(IsolineCollectionProperty); + if (collection == null) return; + + var bounds = ViewportPanel.GetViewportBounds(this); + if (bounds.IsEmpty) return; + + var dc = drawingContext; + var strokeThickness = StrokeThickness; + + var transform = Plotter2D.Transform.WithRects(bounds, new Rect(RenderSize)); + + //dc.DrawRectangle(null, new Pen(Brushes.Green, 2), new Rect(RenderSize)); + + var additionalLevels = GetAdditionalLevels(collection); + IsolineBuilder.DataSource = DataSource; + var additionalIsolineCollections = additionalLevels.Select(level => + { + return IsolineBuilder.BuildIsoline(level); + }); + + foreach (var additionalCollection in additionalIsolineCollections) + { + RenderIsolineCollection(dc, strokeThickness, additionalCollection, transform); + } + } + } +} diff --git a/Charts/Isolines/CellInfo.cs b/Charts/Isolines/CellInfo.cs new file mode 100644 index 0000000..133f683 --- /dev/null +++ b/Charts/Isolines/CellInfo.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + /// + /// Isoline's grid cell + /// + internal interface ICell + { + Vector LeftTop { get; } + Vector LeftBottom { get; } + Vector RightTop { get; } + Vector RightBottom { get; } + } + + internal sealed class IrregularCell : ICell + { + public IrregularCell(Vector leftBottom, Vector rightBottom, Vector rightTop, Vector leftTop) + { + this.leftBottom = leftBottom; + this.rightBottom = rightBottom; + this.rightTop = rightTop; + this.leftTop = leftTop; + } + + public IrregularCell(Point lb, Point rb, Point rt, Point lt) + { + leftTop = lt.ToVector(); + leftBottom = lb.ToVector(); + rightTop = rt.ToVector(); + rightBottom = rb.ToVector(); + } + + #region ICell Members + + private readonly Vector leftTop; + public Vector LeftTop + { + get { return leftTop; } + } + + private readonly Vector leftBottom; + public Vector LeftBottom + { + get { return leftBottom; } + } + + private readonly Vector rightTop; + public Vector RightTop + { + get { return rightTop; } + } + + private readonly Vector rightBottom; + public Vector RightBottom + { + get { return rightBottom; } + } + + #endregion + + #region Sides + public Vector LeftSide + { + get { return (leftBottom + leftTop) / 2; } + } + + public Vector RightSide + { + get { return (rightBottom + rightTop) / 2; } + } + public Vector TopSide + { + get { return (leftTop + rightTop) / 2; } + } + public Vector BottomSide + { + get { return (leftBottom + rightBottom) / 2; } + } + #endregion + + public Point Center + { + get { return ((LeftSide + RightSide) / 2).ToPoint(); } + } + + public IrregularCell GetSubRect(SubCell sub) + { + switch (sub) + { + case SubCell.LeftBottom: + return new IrregularCell(LeftBottom, BottomSide, Center.ToVector(), LeftSide); + case SubCell.LeftTop: + return new IrregularCell(LeftSide, Center.ToVector(), TopSide, LeftTop); + case SubCell.RightBottom: + return new IrregularCell(BottomSide, RightBottom, RightSide, Center.ToVector()); + case SubCell.RightTop: + default: + return new IrregularCell(Center.ToVector(), RightSide, RightTop, TopSide); + } + } + } + + internal enum SubCell + { + LeftBottom = 0, + LeftTop = 1, + RightBottom = 2, + RightTop = 3 + } + + internal class ValuesInCell + { + double min = Double.MaxValue, max = Double.MinValue; + + /// Initializes values in four corners of cell + /// + /// + /// + /// + /// Some or all values can be NaN. That means that value is not specified (misssing) + public ValuesInCell(double leftBottom, double rightBottom, double rightTop, double leftTop) + { + this.leftTop = leftTop; + this.leftBottom = leftBottom; + this.rightTop = rightTop; + this.rightBottom = rightBottom; + + // Find max and min values (with respect to possible NaN values) + if (!Double.IsNaN(leftTop)) + { + if (min > leftTop) + min = leftTop; + if (max < leftTop) + max = leftTop; + } + + if (!Double.IsNaN(leftBottom)) + { + if (min > leftBottom) + min = leftBottom; + if (max < leftBottom) + max = leftBottom; + } + + if (!Double.IsNaN(rightTop)) + { + if (min > rightTop) + min = rightTop; + if (max < rightTop) + max = rightTop; + } + + if (!Double.IsNaN(rightBottom)) + { + if (min > rightBottom) + min = rightBottom; + if (max < rightBottom) + max = rightBottom; + } + + left = (leftTop + leftBottom) / 2; + bottom = (leftBottom + rightBottom) / 2; + right = (rightTop + rightBottom) / 2; + top = (rightTop + leftTop) / 2; + } + + public ValuesInCell(double leftBottom, double rightBottom, double rightTop, double leftTop, double missingValue) + { + DebugVerify.IsNotNaN(leftBottom); + DebugVerify.IsNotNaN(rightBottom); + DebugVerify.IsNotNaN(rightTop); + DebugVerify.IsNotNaN(leftTop); + + // Copy values and find min and max with respect to possible missing values + if (leftTop != missingValue) + { + this.leftTop = leftTop; + if (min > leftTop) + min = leftTop; + if (max < leftTop) + max = leftTop; + } + else + this.leftTop = Double.NaN; + + if (leftBottom != missingValue) + { + this.leftBottom = leftBottom; + if (min > leftBottom) + min = leftBottom; + if (max < leftBottom) + max = leftBottom; + } + else + this.leftBottom = Double.NaN; + + if (rightTop != missingValue) + { + this.rightTop = rightTop; + if (min > rightTop) + min = rightTop; + if (max < rightTop) + max = rightTop; + } + else + this.rightTop = Double.NaN; + + if (rightBottom != missingValue) + { + this.rightBottom = rightBottom; + if (min > rightBottom) + min = rightBottom; + if (max < rightBottom) + max = rightBottom; + } + else + this.rightBottom = Double.NaN; + + left = (this.leftTop + this.leftBottom) / 2; + bottom = (this.leftBottom + this.rightBottom) / 2; + right = (this.rightTop + this.rightBottom) / 2; + top = (this.rightTop + this.leftTop) / 2; + + +/* + if (leftTop != missingValue && ) + { + if (leftBottom != missingValue) + left = (leftTop + leftBottom) / 2; + else + left = Double.NaN; + + if (rightTop != missingValue) + top = (leftTop + rightTop) / 2; + else + top = Double.NaN; + } + + if (rightBottom != missingValue) + { + if (leftBottom != missingValue) + bottom = (leftBottom + rightBottom) / 2; + else + bottom = Double.NaN; + + if (rightTop != missingValue) + right = (rightTop + rightBottom) / 2; + else + right = Double.NaN; + }*/ + } + + + /*internal bool ValueBelongTo(double value) + { + IEnumerable values = new double[] { leftTop, leftBottom, rightTop, rightBottom }; + + return !(values.All(v => v > value) || values.All(v => v < value)); + }*/ + + internal bool ValueBelongTo(double value) + { + return (min <= value && value <= max); + } + + #region Edges + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double leftTop; + public double LeftTop { get { return leftTop; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double leftBottom; + public double LeftBottom { get { return leftBottom; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double rightTop; + public double RightTop + { + get { return rightTop; } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double rightBottom; + public double RightBottom + { + get { return rightBottom; } + } + #endregion + + #region Sides & center + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double left; + public double Left + { + get { return left; } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double right; + public double Right + { + get { return right; } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double top; + public double Top + { + get { return top; } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly double bottom; + public double Bottom + { + get { return bottom; } + } + + public double Center + { + get { return (Left + Right) * 0.5; } + } + #endregion + + #region SubCells + public ValuesInCell LeftTopCell + { + get { return new ValuesInCell(Left, Center, Top, LeftTop); } + } + + public ValuesInCell RightTopCell + { + get { return new ValuesInCell(Center, Right, RightTop, Top); } + } + + public ValuesInCell RightBottomCell + { + get { return new ValuesInCell(Bottom, RightBottom, Right, Center); } + } + + public ValuesInCell LeftBottomCell + { + get { return new ValuesInCell(LeftBottom, Bottom, Center, Left); } + } + + public ValuesInCell GetSubCell(SubCell subCell) + { + switch (subCell) + { + case SubCell.LeftBottom: + return LeftBottomCell; + case SubCell.LeftTop: + return LeftTopCell; + case SubCell.RightBottom: + return RightBottomCell; + case SubCell.RightTop: + default: + return RightTopCell; + } + } + + #endregion + + /// + /// Returns bitmask of comparison of values at cell corners with reference value. + /// Corresponding bit is set to one if value at cell corner is greater than reference value. + /// a------b + /// | Cell | + /// d------c + /// + /// Value at corner (see figure) + /// Value at corner (see figure) + /// Value at corner (see figure) + /// Value at corner (see figure) + /// Reference value + /// Bitmask + public CellBitmask GetCellValue(double value) + { + CellBitmask n = CellBitmask.None; + if (!Double.IsNaN(leftTop) && leftTop > value) + n |= CellBitmask.LeftTop; + if (!Double.IsNaN(leftBottom) && leftBottom > value) + n |= CellBitmask.LeftBottom; + if (!Double.IsNaN(rightBottom) && rightBottom > value) + n |= CellBitmask.RightBottom; + if (!Double.IsNaN(rightTop) && rightTop > value) + n |= CellBitmask.RightTop; + + return n; + } + } +} diff --git a/Charts/Isolines/Enums.cs b/Charts/Isolines/Enums.cs new file mode 100644 index 0000000..e71f414 --- /dev/null +++ b/Charts/Isolines/Enums.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + /// + /// Edge identifier - indicates which side of cell isoline crosses. + /// + internal enum Edge + { + // todo check if everything is ok with None. + None = 0, + /// + /// Isoline crosses left boundary of cell (bit 0) + /// + Left = 1, + /// + /// Isoline crosses top boundary of cell (bit 1) + /// + Top = 2, + /// + /// Isoline crosses right boundary of cell (bit 2) + /// + Right = 4, + /// + /// Isoline crosses bottom boundary of cell (bit 3) + /// + Bottom = 8 + } + + [Flags] + internal enum CellBitmask + { + None = 0, + LeftTop = 1, + LeftBottom = 8, + RightBottom = 4, + RightTop = 2 + } + + internal static class IsolineExtensions + { + internal static bool IsDiagonal(this CellBitmask bitmask) + { + return bitmask == (CellBitmask.RightBottom | CellBitmask.LeftTop) || + bitmask == (CellBitmask.LeftBottom | CellBitmask.RightTop); + } + + internal static bool IsAppropriate(this SubCell sub, Edge edge) + { + switch (sub) + { + case SubCell.LeftBottom: + return edge == Edge.Left || edge == Edge.Bottom; + case SubCell.LeftTop: + return edge == Edge.Left || edge == Edge.Top; + case SubCell.RightBottom: + return edge == Edge.Right || edge == Edge.Bottom; + case SubCell.RightTop: + default: + return edge == Edge.Right || edge == Edge.Top; + } + } + } +} diff --git a/Charts/Isolines/FastIsolineDisplay.xaml b/Charts/Isolines/FastIsolineDisplay.xaml new file mode 100644 index 0000000..30e724f --- /dev/null +++ b/Charts/Isolines/FastIsolineDisplay.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/Charts/Isolines/FastIsolineDisplay.xaml.cs b/Charts/Isolines/FastIsolineDisplay.xaml.cs new file mode 100644 index 0000000..cfb4d9c --- /dev/null +++ b/Charts/Isolines/FastIsolineDisplay.xaml.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using Microsoft.Research.DynamicDataDisplay.Charts.Shapes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + public partial class FastIsolineDisplay : IsolineGraphBase + { + public FastIsolineDisplay() + { + InitializeComponent(); + } + + protected override Panel HostPanel + { + get + { + return Plotter2D.CentralGrid; + } + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + var isolineRenderer = (FastIsolineRenderer)Template.FindName("PART_IsolineRenderer", this); + //Binding contentBoundsBinding = new Binding { Path = new PropertyPath("(0)", Viewport2D.ContentBoundsProperty), Source = isolineRenderer }; + //SetBinding(Viewport2D.ContentBoundsProperty, contentBoundsBinding); + + if (isolineRenderer != null) + { + isolineRenderer.AddHandler(Viewport2D.ContentBoundsChangedEvent, new RoutedEventHandler(OnRendererContentBoundsChanged)); + UpdateContentBounds(isolineRenderer); + } + } + + private void OnRendererContentBoundsChanged(object sender, RoutedEventArgs e) + { + UpdateContentBounds((DependencyObject)sender); + } + + private void UpdateContentBounds(DependencyObject source) + { + var contentBounds = Viewport2D.GetContentBounds(source); + Viewport2D.SetContentBounds(this, contentBounds); + } + } +} diff --git a/Charts/Isolines/FastIsolineRenderer.cs b/Charts/Isolines/FastIsolineRenderer.cs new file mode 100644 index 0000000..163139b --- /dev/null +++ b/Charts/Isolines/FastIsolineRenderer.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Media; +using System.Windows; +using System.Diagnostics; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.Charts.Shapes; +using System.Windows.Threading; +using System.Globalization; +using Microsoft.Research.DynamicDataDisplay.Charts.NewLine; +using System.Windows.Data; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + public class FastIsolineRenderer : IsolineRenderer + { + private List additionalLines = new List(); + private const int subDivisionNum = 10; + + protected override void CreateUIRepresentation() + { + InvalidateVisual(); + } + + protected override void OnPlotterAttached() + { + base.OnPlotterAttached(); + + FrameworkElement parent = (FrameworkElement)Parent; + Binding collectionBinding = new Binding("IsolineCollection") { Source = this }; + parent.SetBinding(IsolineCollectionProperty, collectionBinding); + } + + protected override void OnRender(DrawingContext drawingContext) + { + if (Plotter2D == null) return; + if (Collection == null) return; + if (DataSource == null) return; + if (Collection.Lines.Count == 0) + { + IsolineBuilder.DataSource = DataSource; + IsolineBuilder.MissingValue = MissingValue; + Collection = IsolineBuilder.BuildIsoline(); + } + + IsolineCollection = Collection; + + var dc = drawingContext; + var strokeThickness = StrokeThickness; + var collection = Collection; + + var bounds = DataRect.Empty; + // determining content bounds + foreach (LevelLine line in collection) + { + foreach (Point point in line.AllPoints) + { + bounds.Union(point); + } + } + + Viewport2D.SetContentBounds(this, bounds); + ViewportPanel.SetViewportBounds(this, bounds); + + if (bounds.IsEmpty) return; + + // custom transform with output set to renderSize of this control + var transform = Plotter2D.Transform.WithRects(bounds, new Rect(RenderSize)); + + // actual drawing of isolines + RenderIsolineCollection(dc, strokeThickness, collection, transform); + + //var additionalLevels = GetAdditionalIsolines(collection); + + //var additionalIsolineCollections = additionalLevels.Select(level => IsolineBuilder.BuildIsoline(level)); + + //foreach (var additionalCollection in additionalIsolineCollections) + //{ + // RenderIsolineCollection(dc, strokeThickness, additionalCollection, transform); + //} + + RenderLabels(dc, collection); + + // foreach (var additionalCollection in additionalIsolineCollections) + // { + // RenderLabels(dc, additionalCollection); + // } + } + + private IEnumerable GetAdditionalIsolines(IsolineCollection collection) + { + var dataSource = DataSource; + var visibleMinMax = dataSource.GetMinMax(Plotter2D.Visible); + var visibleMinMaxRatio = (collection.Max - collection.Min) / visibleMinMax.GetLength(); + + var log = Math.Log10(visibleMinMaxRatio); + if (log > 0.9) + { + var upperLog = Math.Ceiling(log); + var divisionsNum = Math.Pow(10, upperLog); + var delta = (collection.Max - collection.Min) / divisionsNum; + + var start = Math.Ceiling(visibleMinMax.Min / delta) * delta; + + var x = start; + while (x < visibleMinMax.Max) + { + yield return x; + x += delta; + } + } + } + + private void RenderLabels(DrawingContext dc, IsolineCollection collection) + { + if (Plotter2D == null) return; + if (collection == null) return; + if (!DrawLabels) return; + + var viewportBounds = ViewportPanel.GetViewportBounds(this); + if (viewportBounds.IsEmpty) + return; + + var strokeThickness = StrokeThickness; + var visible = Plotter2D.Visible; + var output = Plotter2D.Viewport.Output; + + var transform = Plotter2D.Transform.WithRects(viewportBounds, new Rect(RenderSize)); + var labelStringFormat = LabelStringFormat; + + // drawing constants + var labelRectangleFill = Brushes.White; + + var biggerViewport = viewportBounds.ZoomOutFromCenter(1.1); + + // getting and filtering annotations to draw only visible ones + Annotater.WayBeforeText = Math.Sqrt(visible.Width * visible.Width + visible.Height * visible.Height) / 100 * WayBeforeTextMultiplier; + var annotations = Annotater.Annotate(collection, visible) + .Where(annotation => + { + Point viewportPosition = annotation.Position.DataToViewport(transform); + return biggerViewport.Contains(viewportPosition); + }); + + // drawing annotations + foreach (var annotation in annotations) + { + FormattedText text = CreateFormattedText(annotation.Value.ToString(LabelStringFormat)); + Point position = annotation.Position.DataToScreen(transform); + + var labelTransform = CreateTransform(annotation, text, position); + + // creating rectange stroke + double colorRatio = (annotation.Value - collection.Min) / (collection.Max - collection.Min); + colorRatio = MathHelper.Clamp(colorRatio); + Color rectangleStrokeColor = Palette.GetColor(colorRatio); + SolidColorBrush rectangleStroke = new SolidColorBrush(rectangleStrokeColor); + Pen labelRectangleStrokePen = new Pen(rectangleStroke, 2); + + dc.PushTransform(labelTransform); + { + var bounds = RectExtensions.FromCenterSize(position, new Size(text.Width, text.Height)); + bounds = bounds.ZoomOutFromCenter(1.3); + dc.DrawRoundedRectangle(labelRectangleFill, labelRectangleStrokePen, bounds, 8, 8); + + DrawTextInPosition(dc, text, position); + } + dc.Pop(); + } + } + + private static void DrawTextInPosition(DrawingContext dc, FormattedText text, Point position) + { + var textPosition = position; + textPosition.Offset(-text.Width / 2, -text.Height / 2); + dc.DrawText(text, textPosition); + } + + private static Transform CreateTransform(IsolineTextLabel isolineLabel, FormattedText text, Point position) + { + double angle = isolineLabel.Rotation; + if (angle < 0) + angle += 360; + if (90 < angle && angle < 270) + angle -= 180; + + RotateTransform transform = new RotateTransform(angle, position.X, position.Y); + return transform; + } + + private static FormattedText CreateFormattedText(string text) + { + FormattedText result = new FormattedText(text, + CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, new Typeface("Arial"), 12, Brushes.Black,1.0); + return result; + } + } +} diff --git a/Charts/Isolines/IsolineBuilder.cs b/Charts/Isolines/IsolineBuilder.cs new file mode 100644 index 0000000..a5de582 --- /dev/null +++ b/Charts/Isolines/IsolineBuilder.cs @@ -0,0 +1,708 @@ +using System; +using System.Linq; +using System.Diagnostics; +using System.Runtime.Serialization; +using System.Windows; +using System.Windows.Media; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.DataSources; +using System.Collections.Generic; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + /// + /// Generates geometric object for isolines of the input 2d scalar field. + /// + public sealed class IsolineBuilder + { + /// + /// The density of isolines means the number of levels to draw. + /// + private int density = 12; + + private bool[,] processed; + + /// Number to be treated as missing value. NaN if no missing value is specified + private double missingValue = Double.NaN; + + static IsolineBuilder() + { + SetCellDictionaries(); + } + + /// + /// Initializes a new instance of the class. + /// + public IsolineBuilder() { } + + /// + /// Initializes a new instance of the class for specified 2d scalar data source. + /// + /// The data source with 2d scalar data. + public IsolineBuilder(IDataSource2D dataSource) + { + DataSource = dataSource; + } + + public double MissingValue + { + get + { + return missingValue; + } + set + { + missingValue = value; + } + } + + #region Private methods + + private static Dictionary> dictChooser = new Dictionary>(); + private static void SetCellDictionaries() + { + var bottomDict = new Dictionary(); + bottomDict.Add((int)CellBitmask.RightBottom, Edge.Right); + bottomDict.Add(Edge.Left, + CellBitmask.LeftTop, + CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.RightTop, + CellBitmask.LeftTop | CellBitmask.RightBottom | CellBitmask.RightTop, + CellBitmask.LeftBottom); + bottomDict.Add(Edge.Right, + CellBitmask.RightTop, + CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.LeftTop, + CellBitmask.LeftBottom | CellBitmask.LeftTop | CellBitmask.RightTop); + bottomDict.Add(Edge.Top, + CellBitmask.RightBottom | CellBitmask.RightTop, + CellBitmask.LeftBottom | CellBitmask.LeftTop); + + var leftDict = new Dictionary(); + leftDict.Add(Edge.Top, + CellBitmask.LeftTop, + CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.RightTop); + leftDict.Add(Edge.Right, + CellBitmask.LeftTop | CellBitmask.RightTop, + CellBitmask.LeftBottom | CellBitmask.RightBottom); + leftDict.Add(Edge.Bottom, + CellBitmask.RightBottom | CellBitmask.RightTop | CellBitmask.LeftTop, + CellBitmask.LeftBottom); + + var topDict = new Dictionary(); + topDict.Add(Edge.Right, + CellBitmask.RightTop, + CellBitmask.LeftTop | CellBitmask.LeftBottom | CellBitmask.RightBottom); + topDict.Add(Edge.Right, + CellBitmask.RightBottom, + CellBitmask.LeftTop | CellBitmask.LeftBottom | CellBitmask.RightTop); + topDict.Add(Edge.Left, + CellBitmask.RightBottom | CellBitmask.RightTop | CellBitmask.LeftTop, + CellBitmask.LeftBottom, + CellBitmask.LeftTop, + CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.RightTop); + topDict.Add(Edge.Bottom, + CellBitmask.RightBottom | CellBitmask.RightTop, + CellBitmask.LeftTop | CellBitmask.LeftBottom); + + var rightDict = new Dictionary(); + rightDict.Add(Edge.Top, + CellBitmask.RightTop, + CellBitmask.LeftBottom | CellBitmask.RightBottom | CellBitmask.LeftTop); + rightDict.Add(Edge.Left, + CellBitmask.LeftTop | CellBitmask.RightTop, + CellBitmask.LeftBottom | CellBitmask.RightBottom); + rightDict.Add(Edge.Bottom, + CellBitmask.RightBottom, + CellBitmask.LeftTop | CellBitmask.LeftBottom | CellBitmask.RightTop); + + dictChooser.Add((int)Edge.Left, leftDict); + dictChooser.Add((int)Edge.Right, rightDict); + dictChooser.Add((int)Edge.Bottom, bottomDict); + dictChooser.Add((int)Edge.Top, topDict); + } + + private Edge GetOutEdge(Edge inEdge, ValuesInCell cv, IrregularCell rect, double value) + { + // value smaller than all values in corners or + // value greater than all values in corners + if (!cv.ValueBelongTo(value)) + { + throw new IsolineGenerationException(Strings.Exceptions.IsolinesValueIsOutOfCell); + } + + CellBitmask cellVal = cv.GetCellValue(value); + var dict = dictChooser[(int)inEdge]; + if (dict.ContainsKey((int)cellVal)) + { + Edge result = dict[(int)cellVal]; + switch (result) + { + case Edge.Left: + if (cv.LeftTop.IsNaN() || cv.LeftBottom.IsNaN()) + result = Edge.None; + break; + case Edge.Right: + if (cv.RightTop.IsNaN() || cv.RightBottom.IsNaN()) + result = Edge.None; + break; + case Edge.Top: + if (cv.RightTop.IsNaN() || cv.LeftTop.IsNaN()) + result = Edge.None; + break; + case Edge.Bottom: + if (cv.LeftBottom.IsNaN() || cv.RightBottom.IsNaN()) + result = Edge.None; + break; + } + return result; + } + else if (cellVal.IsDiagonal()) + { + return GetOutForOpposite(inEdge, cellVal, value, cv, rect); + } + + const double near_zero = 0.0001; + const double near_one = 1 - near_zero; + + double lt = cv.LeftTop; + double rt = cv.RightTop; + double rb = cv.RightBottom; + double lb = cv.LeftBottom; + + switch (inEdge) + { + case Edge.Left: + if (value == lt) + value = near_one * lt + near_zero * lb; + else if (value == lb) + value = near_one * lb + near_zero * lt; + else + return Edge.None; + // Now this is possible because of missing value + //throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase); + break; + case Edge.Top: + if (value == rt) + value = near_one * rt + near_zero * lt; + else if (value == lt) + value = near_one * lt + near_zero * rt; + else + return Edge.None; + // Now this is possibe because of missing value + //throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase); + break; + case Edge.Right: + if (value == rb) + value = near_one * rb + near_zero * rt; + else if (value == rt) + value = near_one * rt + near_zero * rb; + else + return Edge.None; + // Now this is possibe because of missing value + //throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase); + break; + case Edge.Bottom: + if (value == rb) + value = near_one * rb + near_zero * lb; + else if (value == lb) + value = near_one * lb + near_zero * rb; + else + return Edge.None; + // Now this is possibe because of missing value + //throw new IsolineGenerationException(Strings.Exceptions.IsolinesUnsupportedCase); + break; + } + + // Recursion? + //return GetOutEdge(inEdge, cv, rect, value); + + return Edge.None; + } + + private Edge GetOutForOpposite(Edge inEdge, CellBitmask cellVal, double value, ValuesInCell cellValues, IrregularCell rect) + { + Edge outEdge; + + SubCell subCell = GetSubCell(inEdge, value, cellValues); + + int iters = 1000; // max number of iterations + do + { + ValuesInCell subValues = cellValues.GetSubCell(subCell); + IrregularCell subRect = rect.GetSubRect(subCell); + outEdge = GetOutEdge(inEdge, subValues, subRect, value); + if (outEdge == Edge.None) + return Edge.None; + bool isAppropriate = subCell.IsAppropriate(outEdge); + if (isAppropriate) + { + ValuesInCell sValues = subValues.GetSubCell(subCell); + + Point point = GetPointXY(outEdge, value, subValues, subRect); + segments.AddPoint(point); + return outEdge; + } + else + { + subCell = GetAdjacentEdge(subCell, outEdge); + } + + byte e = (byte)outEdge; + inEdge = (Edge)((e > 2) ? (e >> 2) : (e << 2)); + iters--; + } while (iters >= 0); + + throw new IsolineGenerationException(Strings.Exceptions.IsolinesDataIsUndetailized); + } + + private static SubCell GetAdjacentEdge(SubCell sub, Edge edge) + { + SubCell res = SubCell.LeftBottom; + + switch (sub) + { + case SubCell.LeftBottom: + res = edge == Edge.Top ? SubCell.LeftTop : SubCell.RightBottom; + break; + case SubCell.LeftTop: + res = edge == Edge.Bottom ? SubCell.LeftBottom : SubCell.RightTop; + break; + case SubCell.RightBottom: + res = edge == Edge.Top ? SubCell.RightTop : SubCell.LeftBottom; + break; + case SubCell.RightTop: + default: + res = edge == Edge.Bottom ? SubCell.RightBottom : SubCell.LeftTop; + break; + } + + return res; + } + + private static SubCell GetSubCell(Edge inEdge, double value, ValuesInCell vc) + { + double lb = vc.LeftBottom; + double rb = vc.RightBottom; + double rt = vc.RightTop; + double lt = vc.LeftTop; + + SubCell res = SubCell.LeftBottom; + switch (inEdge) + { + case Edge.Left: + res = (Math.Abs(value - lb) < Math.Abs(value - lt)) ? SubCell.LeftBottom : SubCell.LeftTop; + break; + case Edge.Top: + res = (Math.Abs(value - lt) < Math.Abs(value - rt)) ? SubCell.LeftTop : SubCell.RightTop; + break; + case Edge.Right: + res = (Math.Abs(value - rb) < Math.Abs(value - rt)) ? SubCell.RightBottom : SubCell.RightTop; + break; + case Edge.Bottom: + default: + res = (Math.Abs(value - lb) < Math.Abs(value - rb)) ? SubCell.LeftBottom : SubCell.RightBottom; + break; + } + + ValuesInCell subValues = vc.GetSubCell(res); + bool valueInside = subValues.ValueBelongTo(value); + if (!valueInside) + { + throw new IsolineGenerationException(Strings.Exceptions.IsolinesDataIsUndetailized); + } + + return res; + } + + private static Point GetPoint(double value, double a1, double a2, Vector v1, Vector v2) + { + double ratio = (value - a1) / (a2 - a1); + + Verify.IsTrue(0 <= ratio && ratio <= 1); + + Vector r = (1 - ratio) * v1 + ratio * v2; + return new Point(r.X, r.Y); + } + + private Point GetPointXY(Edge edge, double value, ValuesInCell vc, IrregularCell rect) + { + double lt = vc.LeftTop; + double lb = vc.LeftBottom; + double rb = vc.RightBottom; + double rt = vc.RightTop; + + switch (edge) + { + case Edge.Left: + return GetPoint(value, lb, lt, rect.LeftBottom, rect.LeftTop); + case Edge.Top: + return GetPoint(value, lt, rt, rect.LeftTop, rect.RightTop); + case Edge.Right: + return GetPoint(value, rb, rt, rect.RightBottom, rect.RightTop); + case Edge.Bottom: + return GetPoint(value, lb, rb, rect.LeftBottom, rect.RightBottom); + default: + throw new InvalidOperationException(); + } + } + + private bool BelongsToEdge(double value, double edgeValue1, double edgeValue2, bool onBoundary) + { + if (!Double.IsNaN(missingValue) && (edgeValue1 == missingValue || edgeValue2 == missingValue)) + return false; + + if (onBoundary) + { + return (edgeValue1 <= value && value < edgeValue2) || + (edgeValue2 <= value && value < edgeValue1); + } + else + { + return (edgeValue1 < value && value < edgeValue2) || + (edgeValue2 < value && value < edgeValue1); + } + } + + private bool IsPassed(Edge edge, int i, int j, byte[,] edges) + { + switch (edge) + { + case Edge.Left: + return (i == 0) || (edges[i, j] & (byte)edge) != 0; + case Edge.Bottom: + return (j == 0) || (edges[i, j] & (byte)edge) != 0; + case Edge.Top: + return (j == edges.GetLength(1) - 2) || (edges[i, j + 1] & (byte)Edge.Bottom) != 0; + case Edge.Right: + return (i == edges.GetLength(0) - 2) || (edges[i + 1, j] & (byte)Edge.Left) != 0; + default: + throw new InvalidOperationException(); + } + } + + private void MakeEdgePassed(Edge edge, int i, int j) + { + switch (edge) + { + case Edge.Left: + case Edge.Bottom: + edges[i, j] |= (byte)edge; + break; + case Edge.Top: + edges[i, j + 1] |= (byte)Edge.Bottom; + break; + case Edge.Right: + edges[i + 1, j] |= (byte)Edge.Left; + break; + default: + throw new InvalidOperationException(); + } + } + + private Edge TrackLine(Edge inEdge, double value, ref int x, ref int y, out double newX, out double newY) + { + // Getting output edge + ValuesInCell vc = (missingValue.IsNaN()) ? + (new ValuesInCell(values[x, y], + values[x + 1, y], + values[x + 1, y + 1], + values[x, y + 1])) : + (new ValuesInCell(values[x, y], + values[x + 1, y], + values[x + 1, y + 1], + values[x, y + 1], + missingValue)); + + IrregularCell rect = new IrregularCell( + grid[x, y], + grid[x + 1, y], + grid[x + 1, y + 1], + grid[x, y + 1]); + + Edge outEdge = GetOutEdge(inEdge, vc, rect, value); + if (outEdge == Edge.None) + { + newX = newY = -1; // Impossible cell indices + return Edge.None; + } + + // Drawing new segment + Point point = GetPointXY(outEdge, value, vc, rect); + newX = point.X; + newY = point.Y; + segments.AddPoint(point); + processed[x, y] = true; + + // Whether out-edge already was passed? + if (IsPassed(outEdge, x, y, edges)) // line is closed + { + //MakeEdgePassed(outEdge, x, y); // boundaries should be marked as passed too + return Edge.None; + } + + // Make this edge passed + MakeEdgePassed(outEdge, x, y); + + // Getting next cell's indices + switch (outEdge) + { + case Edge.Left: + x--; + return Edge.Right; + case Edge.Top: + y++; + return Edge.Bottom; + case Edge.Right: + x++; + return Edge.Left; + case Edge.Bottom: + y--; + return Edge.Top; + default: + throw new InvalidOperationException(); + } + } + + private void TrackLineNonRecursive(Edge inEdge, double value, int x, int y) + { + int s = x, t = y; + + ValuesInCell vc = (missingValue.IsNaN()) ? + (new ValuesInCell(values[x, y], + values[x + 1, y], + values[x + 1, y + 1], + values[x, y + 1])) : + (new ValuesInCell(values[x, y], + values[x + 1, y], + values[x + 1, y + 1], + values[x, y + 1], + missingValue)); + + IrregularCell rect = new IrregularCell( + grid[x, y], + grid[x + 1, y], + grid[x + 1, y + 1], + grid[x, y + 1]); + + Point point = GetPointXY(inEdge, value, vc, rect); + + segments.StartLine(point, (value - minMax.Min) / (minMax.Max - minMax.Min), value); + + MakeEdgePassed(inEdge, x, y); + + //processed[x, y] = true; + + double x2, y2; + do + { + inEdge = TrackLine(inEdge, value, ref s, ref t, out x2, out y2); + } while (inEdge != Edge.None); + } + + #endregion + + private bool HasIsoline(int x, int y) + { + return (edges[x,y] != 0 && + ((x < edges.GetLength(0) - 1 && edges[x+1,y] != 0) || + (y < edges.GetLength(1) - 1 && edges[x,y+1] != 0))); + } + + /// Finds isoline for specified reference value + /// Reference value + private void PrepareCells(double value) + { + double currentRatio = (value - minMax.Min) / (minMax.Max - minMax.Min); + + if (currentRatio < 0 || currentRatio > 1) + return; // No contour lines for such value + + int xSize = dataSource.Width; + int ySize = dataSource.Height; + int x, y; + for (x = 0; x < xSize; x++) + for (y = 0; y < ySize; y++) + edges[x, y] = 0; + + processed = new bool[xSize, ySize]; + + // Looking in boundaries. + // left + for (y = 1; y < ySize; y++) + { + if (BelongsToEdge(value, values[0, y - 1], values[0, y], true) && + (edges[0, y - 1] & (byte)Edge.Left) == 0) + { + TrackLineNonRecursive(Edge.Left, value, 0, y - 1); + } + } + + // bottom + for (x = 0; x < xSize - 1; x++) + { + if (BelongsToEdge(value, values[x, 0], values[x + 1, 0], true) + && (edges[x, 0] & (byte)Edge.Bottom) == 0) + { + TrackLineNonRecursive(Edge.Bottom, value, x, 0); + }; + } + + // right + x = xSize - 1; + for (y = 1; y < ySize; y++) + { + // Is this correct? + //if (BelongsToEdge(value, values[0, y - 1], values[0, y], true) && + // (edges[0, y - 1] & (byte)Edge.Left) == 0) + //{ + // TrackLineNonRecursive(Edge.Left, value, 0, y - 1); + //}; + + if (BelongsToEdge(value, values[x, y - 1], values[x, y], true) && + (edges[x, y - 1] & (byte)Edge.Left) == 0) + { + TrackLineNonRecursive(Edge.Right, value, x - 1, y - 1); + }; + } + + // horizontals + for (x = 1; x < xSize - 1; x++) + for (y = 1; y < ySize - 1; y++) + { + if ((edges[x, y] & (byte)Edge.Bottom) == 0 && + BelongsToEdge(value, values[x, y], values[x + 1, y], false) && + !processed[x,y-1]) + { + TrackLineNonRecursive(Edge.Top, value, x, y-1); + } + if ((edges[x, y] & (byte)Edge.Bottom) == 0 && + BelongsToEdge(value, values[x, y], values[x + 1, y], false) && + !processed[x,y]) + { + TrackLineNonRecursive(Edge.Bottom, value, x, y); + } + if ((edges[x, y] & (byte)Edge.Left) == 0 && + BelongsToEdge(value, values[x, y], values[x, y - 1], false) && + !processed[x-1,y-1]) + { + TrackLineNonRecursive(Edge.Right, value, x - 1, y - 1); + } + if ((edges[x, y] & (byte)Edge.Left) == 0 && + BelongsToEdge(value, values[x, y], values[x, y - 1], false) && + !processed[x,y-1]) + { + TrackLineNonRecursive(Edge.Left, value, x, y - 1); + } + } + } + + /// + /// Builds isoline data for 2d scalar field contained in data source. + /// + /// Collection of data describing built isolines. + public IsolineCollection BuildIsoline() + { + VerifyDataSource(); + + segments = new IsolineCollection(); + + minMax = (Double.IsNaN(missingValue) ? dataSource.GetMinMax() : dataSource.GetMinMax(missingValue)); + + segments.Min = minMax.Min; + segments.Max = minMax.Max; + + if (!minMax.IsEmpty) + { + values = dataSource.Data; + double[] levels = GetLevelsForIsolines(); + + foreach (double level in levels) + { + PrepareCells(level); + } + + if (segments.Lines.Count > 0 && segments.Lines[segments.Lines.Count - 1].OtherPoints.Count == 0) + segments.Lines.RemoveAt(segments.Lines.Count - 1); + } + return segments; + } + + /// + /// Builds isoline data for the specified level in 2d scalar field. + /// + /// The level. + /// + public IsolineCollection BuildIsoline(double level) + { + VerifyDataSource(); + + segments = new IsolineCollection(); + + minMax = (Double.IsNaN(missingValue) ? dataSource.GetMinMax() : dataSource.GetMinMax(missingValue)); + if (!minMax.IsEmpty) + { + values = dataSource.Data; + + + PrepareCells(level); + + if (segments.Lines.Count > 0 && segments.Lines[segments.Lines.Count - 1].OtherPoints.Count == 0) + segments.Lines.RemoveAt(segments.Lines.Count - 1); + } + return segments; + } + + private void VerifyDataSource() + { + if (dataSource == null) + throw new InvalidOperationException(Strings.Exceptions.IsolinesDataSourceShouldBeSet); + } + + IsolineCollection segments; + + private double[,] values; + private byte[,] edges; + private Point[,] grid; + + private Range minMax; + private IDataSource2D dataSource; + /// + /// Gets or sets the data source - 2d scalar field. + /// + /// The data source. + public IDataSource2D DataSource + { + get { return dataSource; } + set + { + if (dataSource != value) + { + value.VerifyNotNull("value"); + + dataSource = value; + grid = dataSource.Grid; + edges = new byte[dataSource.Width, dataSource.Height]; + } + } + } + + private const double shiftPercent = 0.05; + private double[] GetLevelsForIsolines() + { + double[] levels; + double min = minMax.Min; + double max = minMax.Max; + + double step = (max - min) / (density - 1); + double delta = (max - min); + + levels = new double[density]; + levels[0] = min + delta * shiftPercent; + levels[levels.Length - 1] = max - delta * shiftPercent; + + for (int i = 1; i < levels.Length - 1; i++) + levels[i] = min + i * step; + + return levels; + } + } +} diff --git a/Charts/Isolines/IsolineCollection.cs b/Charts/Isolines/IsolineCollection.cs new file mode 100644 index 0000000..d767f3e --- /dev/null +++ b/Charts/Isolines/IsolineCollection.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Collections; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + /// + /// LevelLine contains all data for one isoline line - its start point, other points and value in field. + /// + public sealed class LevelLine + { + /// + /// Gets or sets the value of line in limits of [0..1]. + /// + /// The value01. + public double Value01 { get; set; } + /// + /// Gets or sets the real value of line - without scaling to [0..1] segment. + /// + /// The real value. + public double RealValue { get; set; } + + /// + /// Gets or sets the start point of line. + /// + /// The start point. + public Point StartPoint { get; set; } + + private readonly List otherPoints = new List(); + /// + /// Gets other points of line, except first point. + /// + /// The other points. + public List OtherPoints + { + get { return otherPoints; } + } + + /// + /// Gets all points of line, including start point. + /// + /// All points. + public IEnumerable AllPoints + { + get + { + yield return StartPoint; + for (int i = 0; i < otherPoints.Count; i++) + { + yield return otherPoints[i]; + } + } + } + + /// + /// Gets all the segments of lines. + /// + /// + public IEnumerable> GetSegments() + { + if (otherPoints.Count < 1) + yield break; + + yield return new Range(StartPoint, otherPoints[0]); + for (int i = 1; i < otherPoints.Count; i++) + { + yield return new Range(otherPoints[i - 1], otherPoints[i]); + } + } + } + + /// + /// IsolineTextLabel contains information about one label in isoline - its text, position and rotation. + /// + public sealed class IsolineTextLabel + { + /// + /// Gets or sets the rotation of isoline text label. + /// + /// The rotation. + public double Rotation { get; internal set; } + /// + /// Gets or sets the text of isoline label. + /// + /// The text. + public double Value { get; internal set; } + /// + /// Gets or sets the position of isoline text label. + /// + /// The position. + public Point Position { get; internal set; } + } + + /// + /// Collection which contains all data generated by . + /// + public sealed class IsolineCollection : IEnumerable + { + private double min; + public double Min + { + get { return min; } + set { min = value; } + } + + private double max; + public double Max + { + get { return max; } + set { max = value; } + } + + private readonly List lines = new List(); + /// + /// Gets the list of isoline lines. + /// + /// The lines. + public List Lines + { + get { return lines; } + } + + internal void StartLine(Point p, double value01, double realValue) + { + LevelLine segment = new LevelLine { StartPoint = p, Value01 = value01, RealValue = realValue }; + if (lines.Count == 0 || lines[lines.Count - 1].OtherPoints.Count > 0) + lines.Add(segment); + else + lines[lines.Count - 1] = segment; + + } + + internal void AddPoint(Point p) + { + lines[lines.Count - 1].OtherPoints.Add(p); + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return lines.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/Charts/Isolines/IsolineGenerationException.cs b/Charts/Isolines/IsolineGenerationException.cs new file mode 100644 index 0000000..038eb09 --- /dev/null +++ b/Charts/Isolines/IsolineGenerationException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Runtime.Serialization; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + /// + /// Exception that is thrown when error occurs while building isolines. + /// + [Serializable] + public sealed class IsolineGenerationException : Exception + { + internal IsolineGenerationException() { } + internal IsolineGenerationException(string message) : base(message) { } + internal IsolineGenerationException(string message, Exception inner) : base(message, inner) { } + internal IsolineGenerationException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/Charts/Isolines/IsolineGraph.cs b/Charts/Isolines/IsolineGraph.cs new file mode 100644 index 0000000..10f9e70 --- /dev/null +++ b/Charts/Isolines/IsolineGraph.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.Research.DynamicDataDisplay.Charts.Isolines; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.Common; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Draws isolines on given two-dimensional scalar data. + /// + public sealed class IsolineGraph : IsolineRenderer + { + private static Brush labelBackground = new SolidColorBrush(Color.FromArgb(130, 255, 255, 255)); + + /// + /// Initializes a new instance of the class. + /// + public IsolineGraph() + { + Content = content; + Viewport2D.SetIsContentBoundsHost(this, true); + } + + protected override void OnPlotterAttached() + { + CreateUIRepresentation(); + UpdateUIRepresentation(); + } + + private readonly Canvas content = new Canvas(); + + protected override void UpdateDataSource() + { + base.UpdateDataSource(); + + CreateUIRepresentation(); + rebuildText = true; + UpdateUIRepresentation(); + } + + protected override void OnLineThicknessChanged() + { + foreach (var path in linePaths) + { + path.StrokeThickness = StrokeThickness; + } + } + + private List textBlocks = new List(); + private List linePaths = new List(); + protected override void CreateUIRepresentation() + { + if (Plotter2D == null) + return; + + content.Children.Clear(); + linePaths.Clear(); + + if (Collection != null) + { + DataRect bounds = DataRect.Empty; + + foreach (var line in Collection.Lines) + { + foreach (var point in line.AllPoints) + { + bounds.Union(point); + } + + Path path = new Path + { + Stroke = new SolidColorBrush(Palette.GetColor(line.Value01)), + StrokeThickness = StrokeThickness, + Data = CreateGeometry(line), + Tag = line + }; + content.Children.Add(path); + linePaths.Add(path); + } + + Viewport2D.SetContentBounds(this, bounds); + + if (DrawLabels) + { + var transform = Plotter2D.Viewport.Transform; + double wayBeforeText = new Rect(new Size(2000, 2000)).ScreenToData(transform).Width; + Annotater.WayBeforeText = wayBeforeText; + var textLabels = Annotater.Annotate(Collection, Plotter2D.Viewport.Visible); + + foreach (var textLabel in textLabels) + { + var text = CreateTextLabel(textLabel); + content.Children.Add(text); + textBlocks.Add(text); + } + } + } + } + + private FrameworkElement CreateTextLabel(IsolineTextLabel textLabel) + { + var transform = Plotter2D.Viewport.Transform; + Point screenPos = textLabel.Position.DataToScreen(transform); + + double angle = textLabel.Rotation; + if (angle < 0) + angle += 360; + if (135 < angle && angle < 225) + angle -= 180; + + string tooltip = textLabel.Value.ToString("F"); //String.Format("{0} at ({1}, {2})", textLabel.Text, textLabel.Position.X, textLabel.Position.Y); + Grid grid = new Grid + { + RenderTransform = new RotateTransform(angle), + Tag = textLabel, + RenderTransformOrigin = new Point(0.5, 0.5), + ToolTip = tooltip + }; + + TextBlock res = new TextBlock + { + Text = textLabel.Value.ToString("F"), + Margin = new Thickness(3,1,3,1) + }; + + //res.Measure(SizeHelper.CreateInfiniteSize()); + + Rectangle rect = new Rectangle + { + Stroke = Brushes.Gray, + Fill = labelBackground, + RadiusX = 8, + RadiusY = 8 + }; + + grid.Children.Add(rect); + grid.Children.Add(res); + + grid.Measure(SizeHelper.CreateInfiniteSize()); + + Size textSize = grid.DesiredSize; + Point position = new Point(screenPos.X - textSize.Width / 2, screenPos.Y - textSize.Height / 2); + + Canvas.SetLeft(grid, position.X); + Canvas.SetTop(grid, position.Y); + return grid; + } + + private Geometry CreateGeometry(LevelLine lineData) + { + var transform = Plotter2D.Viewport.Transform; + + StreamGeometry geometry = new StreamGeometry(); + using (var context = geometry.Open()) + { + context.BeginFigure(lineData.StartPoint.DataToScreen(transform), false, false); + context.PolyLineTo(lineData.OtherPoints.DataToScreenAsList(transform), true, true); + } + geometry.Freeze(); + return geometry; + } + + private bool rebuildText = true; + protected override void OnViewportPropertyChanged(ExtendedPropertyChangedEventArgs e) + { + if (e.PropertyName == "Visible" || e.PropertyName == "Output") + { + bool isVisibleChanged = e.PropertyName == "Visible"; + DataRect prevRect = isVisibleChanged ? (DataRect)e.OldValue : new DataRect((Rect)e.OldValue); + DataRect currRect = isVisibleChanged ? (DataRect)e.NewValue : new DataRect((Rect)e.NewValue); + + // completely rebuild text only if width and height have changed many + const double smallChangePercent = 0.05; + bool widthChangedLittle = Math.Abs(currRect.Width - prevRect.Width) / currRect.Width < smallChangePercent; + bool heightChangedLittle = Math.Abs(currRect.Height - prevRect.Height) / currRect.Height < smallChangePercent; + + rebuildText = !(widthChangedLittle && heightChangedLittle); + } + UpdateUIRepresentation(); + } + + private void UpdateUIRepresentation() + { + if (Plotter2D == null) return; + + foreach (var path in linePaths) + { + LevelLine line = (LevelLine)path.Tag; + path.Data = CreateGeometry(line); + } + + var transform = Plotter2D.Viewport.Transform; + Rect output = Plotter2D.Viewport.Output; + DataRect visible = Plotter2D.Viewport.Visible; + + if (rebuildText && DrawLabels) + { + rebuildText = false; + foreach (var text in textBlocks) + { + if (text.Visibility == Visibility.Visible) + content.Children.Remove(text); + } + textBlocks.Clear(); + + double wayBeforeText = new Rect(new Size(100, 100)).ScreenToData(transform).Width; + Annotater.WayBeforeText = wayBeforeText; + var textLabels = Annotater.Annotate(Collection, Plotter2D.Viewport.Visible); + foreach (var textLabel in textLabels) + { + var text = CreateTextLabel(textLabel); + textBlocks.Add(text); + if (visible.Contains(textLabel.Position)) + { + content.Children.Add(text); + } + else + { + text.Visibility = Visibility.Hidden; + } + } + } + else + { + foreach (var text in textBlocks) + { + IsolineTextLabel label = (IsolineTextLabel)text.Tag; + Point screenPos = label.Position.DataToScreen(transform); + Size textSize = text.DesiredSize; + + Point position = new Point(screenPos.X - textSize.Width / 2, screenPos.Y - textSize.Height / 2); + + if (output.Contains(position)) + { + Canvas.SetLeft(text, position.X); + Canvas.SetTop(text, position.Y); + if (text.Visibility == Visibility.Hidden) + { + text.Visibility = Visibility.Visible; + content.Children.Add(text); + } + } + else if (text.Visibility == Visibility.Visible) + { + text.Visibility = Visibility.Hidden; + content.Children.Remove(text); + } + } + } + } + } +} diff --git a/Charts/Isolines/IsolineGraphBase.cs b/Charts/Isolines/IsolineGraphBase.cs new file mode 100644 index 0000000..152b3cc --- /dev/null +++ b/Charts/Isolines/IsolineGraphBase.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Common.Palettes; +using Microsoft.Research.DynamicDataDisplay.DataSources; +using DataSource = Microsoft.Research.DynamicDataDisplay.DataSources.IDataSource2D; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Charts.Shapes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + public abstract class IsolineGraphBase : ContentGraph + { + protected IsolineGraphBase() { } + + private IsolineCollection collection = new IsolineCollection(); + protected IsolineCollection Collection + { + get { return collection; } + set { collection = value; } + } + + private readonly IsolineBuilder isolineBuilder = new IsolineBuilder(); + protected IsolineBuilder IsolineBuilder + { + get { return isolineBuilder; } + } + + private readonly IsolineTextAnnotater annotater = new IsolineTextAnnotater(); + protected IsolineTextAnnotater Annotater + { + get { return annotater; } + } + + #region Properties + + #region IsolineCollection property + + public IsolineCollection IsolineCollection + { + get { return (IsolineCollection)GetValue(IsolineCollectionProperty); } + set { SetValue(IsolineCollectionProperty, value); } + } + + public static readonly DependencyProperty IsolineCollectionProperty = DependencyProperty.Register( + "IsolineCollection", + typeof(IsolineCollection), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits)); + + #endregion // end of IsolineCollection property + + #region WayBeforeTextMultiplier + + public double WayBeforeTextMultiplier + { + get { return (double)GetValue(WayBeforeTextMultiplierProperty); } + set { SetValue(WayBeforeTextMultiplierProperty, value); } + } + + public static readonly DependencyProperty WayBeforeTextMultiplierProperty = DependencyProperty.Register( + "WayBeforeTextCoeff", + typeof(double), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged)); + + #endregion // end of WayBeforeTextCoeff + + private static void OnIsolinePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // todo do smth here + } + + #region Palette property + + public IPalette Palette + { + get { return (IPalette)GetValue(PaletteProperty); } + set { SetValue(PaletteProperty, value); } + } + + public static readonly DependencyProperty PaletteProperty = DependencyProperty.Register( + "Palette", + typeof(IPalette), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(new HSBPalette(), FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged), ValidatePalette); + + private static bool ValidatePalette(object value) + { + return value != null; + } + + #endregion // end of Palette property + + #region DataSource property + + public DataSource DataSource + { + get { return (DataSource)GetValue(DataSourceProperty); } + set { SetValue(DataSourceProperty, value); } + } + + public static readonly DependencyProperty DataSourceProperty = DependencyProperty.Register( + "DataSource", + typeof(DataSource), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, OnDataSourceChanged)); + + private static void OnDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + IsolineGraphBase owner = (IsolineGraphBase)d; + owner.OnDataSourceChanged((DataSource)e.OldValue, (DataSource)e.NewValue); + } + + protected virtual void OnDataSourceChanged(IDataSource2D prevDataSource, IDataSource2D currDataSource) + { + if (prevDataSource != null) + prevDataSource.Changed -= OnDataSourceChanged; + if (currDataSource != null) + currDataSource.Changed += OnDataSourceChanged; + + UpdateDataSource(); + CreateUIRepresentation(); + + RaiseEvent(new RoutedEventArgs(BackgroundRenderer.UpdateRequested)); + } + + #endregion // end of DataSource property + + #region DrawLabels property + + public bool DrawLabels + { + get { return (bool)GetValue(DrawLabelsProperty); } + set { SetValue(DrawLabelsProperty, value); } + } + + public static readonly DependencyProperty DrawLabelsProperty = DependencyProperty.Register( + "DrawLabels", + typeof(bool), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged)); + + #endregion // end of DrawLabels property + + #region LabelStringFormat + + public string LabelStringFormat + { + get { return (string)GetValue(LabelStringFormatProperty); } + set { SetValue(LabelStringFormatProperty, value); } + } + + public static readonly DependencyProperty LabelStringFormatProperty = DependencyProperty.Register( + "LabelStringFormat", + typeof(string), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata("F", FrameworkPropertyMetadataOptions.Inherits, OnIsolinePropertyChanged)); + + #endregion // end of LabelStringFormat + + #region UseBezierCurves + + public bool UseBezierCurves + { + get { return (bool)GetValue(UseBezierCurvesProperty); } + set { SetValue(UseBezierCurvesProperty, value); } + } + + public static readonly DependencyProperty UseBezierCurvesProperty = DependencyProperty.Register( + "UseBezierCurves", + typeof(bool), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits)); + + #endregion // end of UseBezierCurves + + #endregion // end of Properties + + #region DataSource + + //private DataSource dataSource = null; + ///// + ///// Gets or sets the data source. + ///// + ///// The data source. + //public DataSource DataSource + //{ + // get { return dataSource; } + // set + // { + // if (dataSource != value) + // { + // DetachDataSource(dataSource); + // dataSource = value; + // AttachDataSource(dataSource); + + // UpdateDataSource(); + // } + // } + //} + + #region MissineValue property + + public double MissingValue + { + get { return (double)GetValue(MissingValueProperty); } + set { SetValue(MissingValueProperty, value); } + } + + public static readonly DependencyProperty MissingValueProperty = DependencyProperty.Register( + "MissingValue", + typeof(double), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(Double.NaN, FrameworkPropertyMetadataOptions.Inherits, OnMissingValueChanged)); + + private static void OnMissingValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + IsolineGraphBase owner = (IsolineGraphBase)d; + owner.UpdateDataSource(); + } + + #endregion // end of MissineValue property + + public void SetDataSource(DataSource dataSource, double missingValue) + { + DataSource = dataSource; + MissingValue = missingValue; + + UpdateDataSource(); + } + + /// + /// This method is called when data source changes. + /// + protected virtual void UpdateDataSource() + { + } + + protected virtual void CreateUIRepresentation() { } + + protected virtual void OnDataSourceChanged(object sender, EventArgs e) + { + UpdateDataSource(); + } + + #endregion + + #region StrokeThickness + + /// + /// Gets or sets thickness of isoline lines. + /// + /// The stroke thickness. + public double StrokeThickness + { + get { return (double)GetValue(StrokeThicknessProperty); } + set { SetValue(StrokeThicknessProperty, value); } + } + + /// + /// Identifies the StrokeThickness dependency property. + /// + public static readonly DependencyProperty StrokeThicknessProperty = + DependencyProperty.Register( + "StrokeThickness", + typeof(double), + typeof(IsolineGraphBase), + new FrameworkPropertyMetadata(2.0, OnLineThicknessChanged)); + + private static void OnLineThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + IsolineGraphBase graph = (IsolineGraphBase)d; + graph.OnLineThicknessChanged(); + } + + protected virtual void OnLineThicknessChanged() { } + + #endregion + } +} diff --git a/Charts/Isolines/IsolineRenderer.cs b/Charts/Isolines/IsolineRenderer.cs new file mode 100644 index 0000000..9aa3b8a --- /dev/null +++ b/Charts/Isolines/IsolineRenderer.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Media; +using System.Windows; +using System.Diagnostics; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.Charts.Shapes; +using System.Windows.Threading; +using System.Globalization; +using Microsoft.Research.DynamicDataDisplay.Charts.NewLine; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + public abstract class IsolineRenderer : IsolineGraphBase + { + protected override void UpdateDataSource() + { + if (DataSource != null) + { + IsolineBuilder.DataSource = DataSource; + IsolineBuilder.MissingValue = MissingValue; + Collection = IsolineBuilder.BuildIsoline(); + } + else + { + Collection = null; + } + } + + protected IEnumerable GetAdditionalLevels(IsolineCollection collection) + { + var dataSource = DataSource; + var visibleMinMax = dataSource.GetMinMax(Plotter2D.Visible); + double totalDelta = collection.Max - collection.Min; + double visibleMinMaxRatio = totalDelta / visibleMinMax.GetLength(); + double defaultDelta = totalDelta / 12; + + if (true || 2 * defaultDelta < visibleMinMaxRatio) + { + double number = Math.Ceiling(visibleMinMaxRatio * 4); + number = Math.Pow(2, Math.Ceiling(Math.Log(number) / Math.Log(2))); + double delta = totalDelta / number; + double x = collection.Min + Math.Ceiling((visibleMinMax.Min - collection.Min) / delta) * delta; + + List result = new List(); + while (x < visibleMinMax.Max) + { + result.Add(x); + x += delta; + } + + return result; + } + + return Enumerable.Empty(); + } + + protected void RenderIsolineCollection(DrawingContext dc, double strokeThickness, IsolineCollection collection, CoordinateTransform transform) + { + foreach (LevelLine line in collection) + { + StreamGeometry lineGeometry = new StreamGeometry(); + using (var context = lineGeometry.Open()) + { + context.BeginFigure(line.StartPoint.ViewportToScreen(transform), false, false); + if (!UseBezierCurves) + { + context.PolyLineTo(line.OtherPoints.ViewportToScreen(transform).ToArray(), true, true); + } + else + { + context.PolyBezierTo(BezierBuilder.GetBezierPoints(line.AllPoints.ViewportToScreen(transform).ToArray()).Skip(1).ToArray(), true, true); + } + } + lineGeometry.Freeze(); + + Pen pen = new Pen(new SolidColorBrush(Palette.GetColor(line.Value01)), strokeThickness); + + dc.DrawGeometry(null, pen, lineGeometry); + } + } + + } +} diff --git a/Charts/Isolines/IsolineTextAnnotater.cs b/Charts/Isolines/IsolineTextAnnotater.cs new file mode 100644 index 0000000..d1daa4b --- /dev/null +++ b/Charts/Isolines/IsolineTextAnnotater.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections.ObjectModel; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Common; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + /// + /// IsolineTextAnnotater defines methods to annotate isolines - create a list of labels with its position. + /// + public sealed class IsolineTextAnnotater + { + private double wayBeforeText = 10; + /// + /// Gets or sets the distance between text labels. + /// + public double WayBeforeText + { + get { return wayBeforeText; } + set { wayBeforeText = value; } + } + + /// + /// Annotates the specified isoline collection. + /// + /// The collection. + /// The visible rectangle. + /// + public Collection Annotate(IsolineCollection collection, DataRect visible) + { + Collection res = new Collection(); + + foreach (var line in collection.Lines) + { + double way = 0; + + var forwardSegments = line.GetSegments(); + var forwardEnumerator = forwardSegments.GetEnumerator(); + forwardEnumerator.MoveNext(); + + foreach (var segment in line.GetSegments()) + { + bool hasForwardSegment = forwardEnumerator.MoveNext(); + + double length = segment.GetLength(); + way += length; + if (way > wayBeforeText) + { + way = 0; + + var rotation = (segment.Max - segment.Min).ToAngle(); + if (hasForwardSegment) + { + var forwardSegment = forwardEnumerator.Current; + rotation = (rotation + (forwardSegment.Max - forwardSegment.Min).ToAngle()) / 2; + } + + res.Add(new IsolineTextLabel + { + Value = line.RealValue, + Position = segment.Max, + Rotation = rotation + }); + } + } + } + + return res; + } + } +} diff --git a/Charts/Isolines/IsolineTrackingGraph.xaml b/Charts/Isolines/IsolineTrackingGraph.xaml new file mode 100644 index 0000000..a69f27a --- /dev/null +++ b/Charts/Isolines/IsolineTrackingGraph.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/Charts/Isolines/IsolineTrackingGraph.xaml.cs b/Charts/Isolines/IsolineTrackingGraph.xaml.cs new file mode 100644 index 0000000..6dc7b8d --- /dev/null +++ b/Charts/Isolines/IsolineTrackingGraph.xaml.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using Microsoft.Research.DynamicDataDisplay.Charts.Isolines; +using Microsoft.Research.DynamicDataDisplay; +using Microsoft.Research.DynamicDataDisplay.Common; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + /// + /// Draws one isoline line through mouse position. + /// + public partial class IsolineTrackingGraph : IsolineGraphBase + { + /// + /// Initializes a new instance of the class. + /// + public IsolineTrackingGraph() + { + InitializeComponent(); + } + + private Style pathStyle = null; + /// + /// Gets or sets style, applied to line path. + /// + /// The path style. + public Style PathStyle + { + get { return pathStyle; } + set + { + pathStyle = value; + foreach (var path in addedPaths) + { + path.Style = pathStyle; + } + } + } + + Point prevMousePos; + + protected override void OnPlotterAttached() + { + UIElement parent = (UIElement)Parent; + parent.MouseMove += parent_MouseMove; + + UpdateUIRepresentation(); + } + + protected override void OnPlotterDetaching() + { + UIElement parent = (UIElement)Parent; + parent.MouseMove -= parent_MouseMove; + } + + private void parent_MouseMove(object sender, MouseEventArgs e) + { + Point mousePos = e.GetPosition(this); + if (mousePos != prevMousePos) + { + prevMousePos = mousePos; + + UpdateUIRepresentation(); + } + } + + protected override void UpdateDataSource() + { + IsolineBuilder.DataSource = DataSource; + + UpdateUIRepresentation(); + } + + protected override void OnViewportPropertyChanged(ExtendedPropertyChangedEventArgs e) + { + UpdateUIRepresentation(); + } + + private readonly List addedPaths = new List(); + private Vector labelShift = new Vector(3, 3); + private void UpdateUIRepresentation() + { + if (Plotter2D == null) + return; + + foreach (var path in addedPaths) + { + content.Children.Remove(path); + } + addedPaths.Clear(); + + if (DataSource == null) + { + labelGrid.Visibility = Visibility.Hidden; + return; + } + + Rect output = Plotter2D.Viewport.Output; + + Point mousePos = Mouse.GetPosition(this); + if (!output.Contains(mousePos)) return; + + var transform = Plotter2D.Viewport.Transform; + Point visiblePoint = mousePos.ScreenToData(transform); + DataRect visible = Plotter2D.Viewport.Visible; + + double isolineLevel; + if (Search(visiblePoint, out isolineLevel)) + { + var collection = IsolineBuilder.BuildIsoline(isolineLevel); + + string format = "G3"; + if (Math.Abs(isolineLevel) < 1000) + format = "F"; + + textBlock.Text = isolineLevel.ToString(format); + + double x = mousePos.X + labelShift.X; + if (x + labelGrid.ActualWidth > output.Right) + x = mousePos.X - labelShift.X - labelGrid.ActualWidth; + double y = mousePos.Y - labelShift.Y - labelGrid.ActualHeight; + if (y < output.Top) + y = mousePos.Y + labelShift.Y; + + Canvas.SetLeft(labelGrid, x); + Canvas.SetTop(labelGrid, y); + + foreach (LevelLine segment in collection.Lines) + { + StreamGeometry streamGeom = new StreamGeometry(); + using (StreamGeometryContext context = streamGeom.Open()) + { + Point startPoint = segment.StartPoint.DataToScreen(transform); + var otherPoints = segment.OtherPoints.DataToScreenAsList(transform); + context.BeginFigure(startPoint, false, false); + context.PolyLineTo(otherPoints, true, true); + } + + Path path = new Path + { + Stroke = new SolidColorBrush(Palette.GetColor(segment.Value01)), + Data = streamGeom, + Style = pathStyle + }; + content.Children.Add(path); + addedPaths.Add(path); + + labelGrid.Visibility = Visibility.Visible; + + Binding pathBinding = new Binding { Path = new PropertyPath("StrokeThickness"), Source = this }; + path.SetBinding(Path.StrokeThicknessProperty, pathBinding); + } + } + else + { + labelGrid.Visibility = Visibility.Hidden; + } + } + + int foundI = 0; + int foundJ = 0; + Quad foundQuad = null; + private bool Search(Point pt, out double foundVal) + { + var grid = DataSource.Grid; + + foundVal = 0; + + int width = DataSource.Width; + int height = DataSource.Height; + bool found = false; + int i = 0, j = 0; + for (i = 0; i < width - 1; i++) + { + for (j = 0; j < height - 1; j++) + { + Quad quad = new Quad( + grid[i, j], + grid[i, j + 1], + grid[i + 1, j + 1], + grid[i + 1, j]); + if (quad.Contains(pt)) + { + found = true; + foundQuad = quad; + foundI = i; + foundJ = j; + + break; + } + } + if (found) break; + } + if (!found) + { + foundQuad = null; + return false; + } + + var data = DataSource.Data; + + double x = pt.X; + double y = pt.Y; + Vector A = grid[i, j + 1].ToVector(); // @TODO: in common case add a sorting of points: + Vector B = grid[i + 1, j + 1].ToVector(); // maxA ___K___ B + Vector C = grid[i + 1, j].ToVector(); // | | + Vector D = grid[i, j].ToVector(); // M P N + double a = data[i, j + 1]; // | | + double b = data[i + 1, j + 1]; // В ___L____Сmin + double c = data[i + 1, j]; + double d = data[i, j]; + + Vector K, L; + double k, l; + if (x >= A.X) + k = Interpolate(A, B, a, b, K = new Vector(x, GetY(A, B, x))); + else + k = Interpolate(D, A, d, a, K = new Vector(x, GetY(D, A, x))); + + if (x >= C.X) + l = Interpolate(C, B, c, b, L = new Vector(x, GetY(C, B, x))); + else + l = Interpolate(D, C, d, c, L = new Vector(x, GetY(D, C, x))); + + foundVal = Interpolate(L, K, l, k, new Vector(x, y)); + return !Double.IsNaN(foundVal); + } + + private double Interpolate(Vector v0, Vector v1, double u0, double u1, Vector a) + { + Vector l1 = a - v0; + Vector l = v1 - v0; + + double res = (u1 - u0) / l.Length * l1.Length + u0; + return res; + } + + private double GetY(Vector v0, Vector v1, double x) + { + double res = v0.Y + (v1.Y - v0.Y) / (v1.X - v0.X) * (x - v0.X); + return res; + } + } +} diff --git a/Charts/Isolines/Quad.cs b/Charts/Isolines/Quad.cs new file mode 100644 index 0000000..ad17668 --- /dev/null +++ b/Charts/Isolines/Quad.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines +{ + /// + /// Represents quadrangle; its points are arranged by round in one direction. + /// + internal sealed class Quad + { + private readonly Point v00; + public Point V00 + { + get { return v00; } + } + + private readonly Point v01; + public Point V01 + { + get { return v01; } + } + + private readonly Point v10; + public Point V10 + { + get { return v10; } + } + + private readonly Point v11; + public Point V11 + { + get { return v11; } + } + + public Quad(Point v00, Point v01, Point v11, Point v10) + { + DebugVerify.IsNotNaN(v00); + DebugVerify.IsNotNaN(v01); + DebugVerify.IsNotNaN(v11); + DebugVerify.IsNotNaN(v10); + + this.v00 = v00; + this.v01 = v01; + this.v10 = v10; + this.v11 = v11; + } + + /// + /// Determines whether this quad contains the specified point. + /// + /// The point + /// + /// true if quad contains the specified point; otherwise, false. + /// + public bool Contains(Point pt) + { + // breaking quad into 2 triangles, + // points contains in quad, if it contains in at least one half-triangle of it. + return TriangleMath.TriangleContains(v00, v01, v11, pt) || TriangleMath.TriangleContains(v00, v10, v11, pt); + } + } +} diff --git a/Charts/Legend items/LegendBottomButtonIsEnabledConverter.cs b/Charts/Legend items/LegendBottomButtonIsEnabledConverter.cs new file mode 100644 index 0000000..0ded3bb --- /dev/null +++ b/Charts/Legend items/LegendBottomButtonIsEnabledConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Converters; +using System.Windows.Controls; +using System.Globalization; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal sealed class LegendBottomButtonIsEnabledConverter : ThreeValuesMultiConverter + { + protected override object ConvertCore(double value1, double value2, double value3, Type targetType, object parameter, CultureInfo culture) + { + double extentHeight = value1; + double viewportHeight = value2; + double offset = value3; + + return viewportHeight < (extentHeight - offset); + } + } +} diff --git a/Charts/Legend items/LegendItemsHelper.cs b/Charts/Legend items/LegendItemsHelper.cs new file mode 100644 index 0000000..6c8bbea --- /dev/null +++ b/Charts/Legend items/LegendItemsHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Data; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Legend_items +{ + public static class LegendItemsHelper + { + public static NewLegendItem BuildDefaultLegendItem(IPlotterElement chart) + { + DependencyObject dependencyChart = (DependencyObject)chart; + + NewLegendItem result = new NewLegendItem(); + SetCommonBindings(result, chart); + return result; + } + + public static void SetCommonBindings(NewLegendItem legendItem, object chart) + { + legendItem.DataContext = chart; + legendItem.SetBinding(NewLegend.VisualContentProperty, new Binding { Path = new PropertyPath("(0)", NewLegend.VisualContentProperty) }); + legendItem.SetBinding(NewLegend.DescriptionProperty, new Binding { Path = new PropertyPath("(0)", NewLegend.DescriptionProperty) }); + } + + } +} diff --git a/Charts/Legend items/LegendResources.xaml b/Charts/Legend items/LegendResources.xaml new file mode 100644 index 0000000..470de84 --- /dev/null +++ b/Charts/Legend items/LegendResources.xaml @@ -0,0 +1,128 @@ + + + + + + --> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Charts/Legend items/LegendStyles.cs b/Charts/Legend items/LegendStyles.cs new file mode 100644 index 0000000..9b41f24 --- /dev/null +++ b/Charts/Legend items/LegendStyles.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public static class LegendStyles + { + private static Style defaultStyle; + public static Style Default + { + get + { + if (defaultStyle == null) + { + var legendStyles = GetLegendStyles(); + defaultStyle = (Style)legendStyles[typeof(NewLegend)]; + } + + return defaultStyle; + } + } + + private static Style noScrollStyle; + public static Style NoScroll + { + get + { + if (noScrollStyle == null) + { + var legendStyles = GetLegendStyles(); + noScrollStyle = (Style)legendStyles["NoScrollLegendStyle"]; + } + + return noScrollStyle; + } + } + + private static ResourceDictionary GetLegendStyles() + { + var legendStyles = (ResourceDictionary)Application.LoadComponent(new Uri("/DynamicDataDisplay;component/Charts/Legend items/LegendResources.xaml", UriKind.Relative)); + return legendStyles; + } + } +} diff --git a/Charts/Legend items/LegendTopButtonToIsEnabledConverter.cs b/Charts/Legend items/LegendTopButtonToIsEnabledConverter.cs new file mode 100644 index 0000000..75a3734 --- /dev/null +++ b/Charts/Legend items/LegendTopButtonToIsEnabledConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Research.DynamicDataDisplay.Converters; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + internal sealed class LegendTopButtonToIsEnabledConverter : GenericValueConverter + { + public override object ConvertCore(double value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + double verticalOffset = value; + + return verticalOffset > 0; + } + } +} diff --git a/Charts/Legend items/NewLegend.cs b/Charts/Legend items/NewLegend.cs new file mode 100644 index 0000000..d05599b --- /dev/null +++ b/Charts/Legend items/NewLegend.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows; +using System.Windows.Data; +using System.Windows.Shapes; +using System.Windows.Media.Effects; +using System.Collections.Specialized; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Collections; +using System.Windows.Media; +using Microsoft.Research.DynamicDataDisplay.Charts.Legend_items; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public delegate IEnumerable LegendItemsBuilder(IPlotterElement element); + + public class NewLegend : ItemsControl, IPlotterElement + { + static NewLegend() + { + Type thisType = typeof(NewLegend); + DefaultStyleKeyProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(thisType)); + + Plotter.PlotterProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(OnPlotterChanged)); + } + + private readonly ObservableCollection legendItems = new ObservableCollection(); + + public NewLegend() + { + ItemsSource = legendItems; + } + + #region IPlotterElement Members + + private static void OnPlotterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + NewLegend legend = (NewLegend)d; + if (e.OldValue != null) + { + legend.DetachFromPlotter((Plotter)e.OldValue); + } + if (e.NewValue != null) + { + legend.AttachToPlotter((Plotter)e.NewValue); + } + } + + private Plotter plotter; + public void OnPlotterAttached(Plotter plotter) + { + plotter.CentralGrid.Children.Add(this); + + AttachToPlotter(plotter); + } + + private void AttachToPlotter(Plotter plotter) + { + if (plotter != this.plotter) + { + this.plotter = plotter; + plotter.Children.CollectionChanged += PlotterChildren_CollectionChanged; + PopulateLegend(); + } + } + + public void OnPlotterDetaching(Plotter plotter) + { + plotter.CentralGrid.Children.Remove(this); + + DetachFromPlotter(plotter); + } + + private void DetachFromPlotter(Plotter plotter) + { + if (plotter != null) + { + plotter.Children.CollectionChanged -= PlotterChildren_CollectionChanged; + this.plotter = null; + CleanLegend(); + } + } + + public Plotter Plotter + { + get { return plotter; } + } + + #endregion + + private void PlotterChildren_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + PopulateLegend(); + } + + public void PopulateLegend() + { + legendItems.Clear(); + +// smk if (!LegendVisible) return; + + foreach (var chart in plotter.Children.OfType()) + { + var legendItemsBuilder = NewLegend.GetLegendItemsBuilder(chart); + if (legendItemsBuilder != null) + { + foreach (var legendItem in legendItemsBuilder((IPlotterElement)chart)) + { + legendItems.Add(legendItem); + } + } + + //var controller = LegendItemControllersStore.Current.GetController(chart.GetType()); + //if (controller != null) + //{ + // controller.ProcessChart(chart, this); + //} + } + +// smk UpdateVisibility(); + } + + private void UpdateVisibility() + { + if (legendItems.Count > 0) + Visibility = Visibility.Visible; + else + Visibility = Visibility.Hidden; + } + + private void CleanLegend() + { + foreach (var legendItem in legendItems) + { + BindingOperations.ClearAllBindings(legendItem); + } + legendItems.Clear(); + + UpdateVisibility(); + } + + #region Attached Properties + + #region Description + + public static object GetDescription(DependencyObject obj) + { + return obj.GetValue(DescriptionProperty); + } + + public static void SetDescription(DependencyObject obj, object value) + { + obj.SetValue(DescriptionProperty, value); + } + + public static readonly DependencyProperty DescriptionProperty = DependencyProperty.RegisterAttached( + "Description", + typeof(object), + typeof(NewLegend), + new FrameworkPropertyMetadata(null)); + + #endregion // end of Description + + #region Detailed description + + public static object GetDetailedDescription(DependencyObject obj) + { + return (object)obj.GetValue(DetailedDescriptionProperty); + } + + public static void SetDetailedDescription(DependencyObject obj, object value) + { + obj.SetValue(DetailedDescriptionProperty, value); + } + + public static readonly DependencyProperty DetailedDescriptionProperty = DependencyProperty.RegisterAttached( + "DetailedDescription", + typeof(object), + typeof(NewLegend), + new FrameworkPropertyMetadata(null)); + + #endregion // end of Detailed description + + #region VisualContent + + public static object GetVisualContent(DependencyObject obj) + { + return (object)obj.GetValue(VisualContentProperty); + } + + public static void SetVisualContent(DependencyObject obj, object value) + { + obj.SetValue(VisualContentProperty, value); + } + + public static readonly DependencyProperty VisualContentProperty = DependencyProperty.RegisterAttached( + "VisualContent", + typeof(object), + typeof(NewLegend), + new FrameworkPropertyMetadata(null)); + + #endregion // end of VisualContent + + #region SampleData + + public static object GetSampleData(DependencyObject obj) + { + return (object)obj.GetValue(SampleDataProperty); + } + + public static void SetSampleData(DependencyObject obj, object value) + { + obj.SetValue(SampleDataProperty, value); + } + + public static readonly DependencyProperty SampleDataProperty = DependencyProperty.RegisterAttached( + "SampleData", + typeof(object), + typeof(NewLegend), + new FrameworkPropertyMetadata(null)); + + #endregion // end of SampleData + + #region ShowInLegend + + public static bool GetShowInLegend(DependencyObject obj) + { + return (bool)obj.GetValue(ShowInLegendProperty); + } + + public static void SetShowInLegend(DependencyObject obj, bool value) + { + obj.SetValue(ShowInLegendProperty, value); + } + + public static readonly DependencyProperty ShowInLegendProperty = DependencyProperty.RegisterAttached( + "ShowInLegend", + typeof(bool), + typeof(NewLegend), + new FrameworkPropertyMetadata(true, OnShowInLegendChanged)); + + private static void OnShowInLegendChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + NewLegend legend = (NewLegend)d; + legend.PopulateLegend(); + } + + #endregion // end of ShowInLegend + + #region LegendItemsBuilder + + public static LegendItemsBuilder GetLegendItemsBuilder(DependencyObject obj) + { + return (LegendItemsBuilder)obj.GetValue(LegendItemsBuilderProperty); + } + + public static void SetLegendItemsBuilder(DependencyObject obj, LegendItemsBuilder value) + { + obj.SetValue(LegendItemsBuilderProperty, value); + } + + public static readonly DependencyProperty LegendItemsBuilderProperty = DependencyProperty.RegisterAttached( + "LegendItemsBuilder", + typeof(LegendItemsBuilder), + typeof(NewLegend), + new FrameworkPropertyMetadata(null, OnLegendItemsBuilderChanged)); + + private static void OnLegendItemsBuilderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + IPlotterElement plotterElement = d as IPlotterElement; + if (plotterElement != null && plotterElement.Plotter != null) + { + ChartPlotter plotter = plotterElement.Plotter as ChartPlotter; + if (plotter != null) + { + plotter.NewLegend.PopulateLegend(); + } + } + } + + #endregion // end of LegendItemsBuilder + + #endregion // end of Attached Properties + + #region Properties + + public bool LegendVisible + { + get { return (bool)GetValue(LegendVisibleProperty); } + set { SetValue(LegendVisibleProperty, value); } + } + + public static readonly DependencyProperty LegendVisibleProperty = DependencyProperty.Register( + "LegendVisible", + typeof(bool), + typeof(NewLegend), + new FrameworkPropertyMetadata(true, OnLegendVisibleChanged)); + + private static void OnLegendVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + NewLegend owner = (NewLegend)d; + + var visible = (bool)e.NewValue; + owner.OnLegendVisibleChanged(visible); + } + + private void OnLegendVisibleChanged(bool visible) + { + if (visible && legendItems.Count > 0) + { + Visibility = Visibility.Visible; + } + else + { + Visibility = Visibility.Collapsed; + } + } + + #endregion // end of Properties + + #region Overrides + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + +#if !RELEASEXBAP + var rect = (Rectangle)Template.FindName("backRect", this); + if (rect != null) + { + rect.Effect = new DropShadowEffect { Direction = 300, ShadowDepth = 3, Opacity = 0.4 }; + } +#endif + } + + #endregion // end of Overrides + } +} diff --git a/Charts/Legend items/NewLegendItem.cs b/Charts/Legend items/NewLegendItem.cs new file mode 100644 index 0000000..31b8d67 --- /dev/null +++ b/Charts/Legend items/NewLegendItem.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows; +using System.Windows.Media.Animation; +using System.ComponentModel; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class NewLegendItem : Control + { + static NewLegendItem() + { + var thisType = typeof(NewLegendItem); + DefaultStyleKeyProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(thisType)); + } + + //public object VisualContent + //{ + // get { return NewLegend.GetVisualContent(this); } + // set { NewLegend.SetVisualContent(this, value); } + //} + + //[Bindable(true)] + //public object Description + //{ + // get { return NewLegend.GetDescription(this); } + // set { NewLegend.SetDescription(this, value); } + //} + } +} diff --git a/Charts/Legend.xaml b/Charts/Legend.xaml new file mode 100644 index 0000000..0e8e09f --- /dev/null +++ b/Charts/Legend.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/Charts/Legend.xaml.cs b/Charts/Legend.xaml.cs new file mode 100644 index 0000000..5a59e93 --- /dev/null +++ b/Charts/Legend.xaml.cs @@ -0,0 +1,301 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media.Effects; +using System; + +namespace Microsoft.Research.DynamicDataDisplay +{ + /// + /// Legend - shows list of annotations to charts. + /// + public partial class Legend : ContentControl, IPlotterElement + { + /// + /// Initializes a new instance of the class. + /// + public Legend() + { + InitializeComponent(); + +#if !RELEASEXBAP + shadowRect.Effect = new DropShadowEffect { Direction = 300, ShadowDepth = 3, Opacity = 0.4 }; +#endif + } + + #region Position properties + + public double LegendLeft + { + get { return (double)GetValue(LegendLeftProperty); } + set { SetValue(LegendLeftProperty, value); } + } + + public static readonly DependencyProperty LegendLeftProperty = DependencyProperty.Register( + "LegendLeft", + typeof(double), + typeof(Legend), + new FrameworkPropertyMetadata(Double.NaN)); + + public double LegendRight + { + get { return (double)GetValue(LegendRightProperty); } + set { SetValue(LegendRightProperty, value); } + } + + public static readonly DependencyProperty LegendRightProperty = DependencyProperty.Register( + "LegendRight", + typeof(double), + typeof(Legend), + new FrameworkPropertyMetadata(10.0)); + + public double LegendBottom + { + get { return (double)GetValue(LegendBottomProperty); } + set { SetValue(LegendBottomProperty, value); } + } + + public static readonly DependencyProperty LegendBottomProperty = DependencyProperty.Register( + "LegendBottom", + typeof(double), + typeof(Legend), + new FrameworkPropertyMetadata(Double.NaN)); + + public double LegendTop + { + get { return (double)GetValue(LegendTopProperty); } + set { SetValue(LegendTopProperty, value); } + } + + public static readonly DependencyProperty LegendTopProperty = DependencyProperty.Register( + "LegendTop", + typeof(double), + typeof(Legend), + new FrameworkPropertyMetadata(10.0)); + + #endregion + + public override bool ShouldSerializeContent() + { + return false; + } + + #region Plotter attached & detached + + private Plotter plotter; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Plotter Plotter + { + get { return plotter; } + } + + void IPlotterElement.OnPlotterAttached(Plotter plotter) + { + this.plotter = plotter; + plotter.Children.CollectionChanged += OnPlotterChildrenChanged; + plotter.CentralGrid.Children.Add(this); + + SubscribeOnEvents(); + PopulateLegend(); + } + + private void SubscribeOnEvents() + { + foreach (var item in plotter.Children.OfType()) + { + item.PropertyChanged += OnChartPropertyChanged; + } + } + + private void OnPlotterChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + ManageEvents(e); + + PopulateLegend(); + } + + private void ManageEvents(NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + foreach (var item in e.OldItems.OfType()) + { + item.PropertyChanged -= OnChartPropertyChanged; + } + } + if (e.NewItems != null) + { + foreach (var item in e.NewItems.OfType()) + { + item.PropertyChanged += OnChartPropertyChanged; + } + } + } + + private void OnChartPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Description") + { + ViewportElement2D chart = sender as ViewportElement2D; + if (chart != null && cachedLegendItems.ContainsKey(chart)) + { + // todo dirty, but quick to code. + PopulateLegend(); + } + } + } + + void IPlotterElement.OnPlotterDetaching(Plotter plotter) + { + UnsubscribeFromEvents(); + plotter.CentralGrid.Children.Remove(this); + plotter.Children.CollectionChanged -= OnPlotterChildrenChanged; + + this.plotter = null; + + PopulateLegend(); + } + + private void UnsubscribeFromEvents() + { + foreach (var item in plotter.Children.OfType()) + { + item.PropertyChanged -= OnChartPropertyChanged; + } + } + + #endregion + + public Grid ContentGrid + { + get { return grid; } + } + + public Panel ContentPanel + { + get { return stackPanel; } + } + + private bool autoShowAndHide = true; + /// + /// Gets or sets a value indicating whether legend automatically shows or hides itself + /// when chart collection changes. + /// + /// true if legend automatically shows and hides itself when chart collection changes; otherwise, false. + public bool AutoShowAndHide + { + get { return autoShowAndHide; } + set { autoShowAndHide = value; } + } + + /// + /// Adds new legend item. + /// + /// The legend item. + public void AddLegendItem(LegendItem legendItem) + { + stackPanel.Children.Add(legendItem); + UpdateVisibility(); + } + + /// + /// Removes the legend item. + /// + /// The legend item. + public void RemoveLegendItem(LegendItem legendItem) + { + stackPanel.Children.Remove(legendItem); + UpdateVisibility(); + } + + private void UpdateVisibility() + { + if (stackPanel.Children.Count > 0 && ReadLocalValue(VisibilityProperty) == DependencyProperty.UnsetValue && autoShowAndHide == true) + { + Visibility = Visibility.Visible; + } + else if (stackPanel.Children.Count == 0 && ReadLocalValue(VisibilityProperty) != DependencyProperty.UnsetValue && autoShowAndHide == true) + { + Visibility = Visibility.Hidden; + } + } + + private readonly Dictionary cachedLegendItems = new Dictionary(); + + private void ParentChartPlotter_CollectionChanged(object sender, CollectionChangeEventArgs e) + { + stackPanel.Children.Clear(); + PopulateLegend(); + } + + private void graph_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Description") + { + ViewportElement2D graph = (ViewportElement2D)sender; + LegendItem oldLegendItem = cachedLegendItems[graph]; + int index = stackPanel.Children.IndexOf(oldLegendItem); + stackPanel.Children.RemoveAt(index); + + LegendItem newLegendItem = graph.Description.LegendItem; + cachedLegendItems[graph] = newLegendItem; + stackPanel.Children.Insert(index, newLegendItem); + } + } + + public void PopulateLegend() + { + stackPanel.Children.Clear(); + + if (plotter == null) + { + return; + } + + cachedLegendItems.Clear(); + foreach (var graph in plotter.Children.OfType()) + { + if (GetVisibleInLegend(graph)) + { + LegendItem legendItem = graph.Description.LegendItem; + cachedLegendItems.Add(graph, legendItem); + AddLegendItem(legendItem); + } + } + + UpdateVisibility(); + } + + #region VisibleInLegend attached dependency property + + public static bool GetVisibleInLegend(DependencyObject obj) + { + return (bool)obj.GetValue(VisibleInLegendProperty); + } + + public static void SetVisibleInLegend(DependencyObject obj, bool value) + { + obj.SetValue(VisibleInLegendProperty, value); + } + + public static readonly DependencyProperty VisibleInLegendProperty = + DependencyProperty.RegisterAttached( + "VisibleInLegend", + typeof(bool), + typeof(Legend), new FrameworkPropertyMetadata(false, OnVisibleInLegendChanged)); + + private static void OnVisibleInLegendChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ChartPlotter plotter = Plotter.GetPlotter(d) as ChartPlotter; + if (plotter != null) + { + plotter.Legend.PopulateLegend(); + } + } + + #endregion + } +} diff --git a/Charts/LegendItem.cs b/Charts/LegendItem.cs new file mode 100644 index 0000000..9917ded --- /dev/null +++ b/Charts/LegendItem.cs @@ -0,0 +1,39 @@ +using System.Windows.Controls; + +namespace Microsoft.Research.DynamicDataDisplay +{ + /// + /// is a base class for item in legend, that represents some chart. + /// + public abstract class LegendItem : ContentControl + { + /// + /// Initializes a new instance of the class. + /// + protected LegendItem() { } + + /// + /// Initializes a new instance of the class. + /// + /// The description. + protected LegendItem(Description description) + { + Description = description; + } + + private Description description; + /// + /// Gets or sets the description. + /// + /// The description. + public Description Description + { + get { return description; } + set + { + description = value; + Content = description; + } + } + } +} diff --git a/Charts/LineAndMarker.cs b/Charts/LineAndMarker.cs new file mode 100644 index 0000000..27db734 --- /dev/null +++ b/Charts/LineAndMarker.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Research.DynamicDataDisplay +{ + public sealed class LineAndMarker + { + public LineGraph LineGraph { get; set; } + public T MarkerGraph { get; set; } + } +} diff --git a/Charts/LineGraph.cs b/Charts/LineGraph.cs new file mode 100644 index 0000000..8ecf25f --- /dev/null +++ b/Charts/LineGraph.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Media; +using Microsoft.Research.DynamicDataDisplay.Charts; +using Microsoft.Research.DynamicDataDisplay.Charts.Legend_items; +using Microsoft.Research.DynamicDataDisplay.DataSources; +using Microsoft.Research.DynamicDataDisplay.Filters; +using System.Windows.Shapes; + + +namespace Microsoft.Research.DynamicDataDisplay +{ + /// + /// Represents a series of points connected by one polyline. + /// + public class LineGraph : PointsGraphBase + { + static LineGraph() + { + Type thisType = typeof(LineGraph); + + NewLegend.DescriptionProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata("LineGraph")); + NewLegend.LegendItemsBuilderProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(new LegendItemsBuilder(DefaultLegendItemsBuilder))); + } + + private static IEnumerable DefaultLegendItemsBuilder(IPlotterElement plotterElement) + { + LineGraph lineGraph = (LineGraph)plotterElement; + + Line line = new Line { X1 = 0, Y1 = 10, X2 = 20, Y2 = 0, Stretch = Stretch.Fill, DataContext = lineGraph }; + line.SetBinding(Line.StrokeProperty, "Stroke"); + line.SetBinding(Line.StrokeThicknessProperty, "StrokeThickness"); + NewLegend.SetVisualContent(lineGraph, line); + + var legendItem = LegendItemsHelper.BuildDefaultLegendItem(lineGraph); + yield return legendItem; + } + + private readonly FilterCollection filters = new FilterCollection(); + + /// + /// Initializes a new instance of the class. + /// + public LineGraph() + { + Legend.SetVisibleInLegend(this, true); + ManualTranslate = true; + + filters.CollectionChanged += filters_CollectionChanged; + } + + private void filters_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + filteredPoints = null; + Update(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The point source. + public LineGraph(IPointDataSource pointSource) + : this() + { + DataSource = pointSource; + } + + protected override Description CreateDefaultDescription() + { + return new PenDescription(); + } + + /// Provides access to filters collection + public FilterCollection Filters + { + get { return filters; } + } + + #region Pen + + /// + /// Gets or sets the brush, using which polyline is plotted. + /// + /// The line brush. + 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; + } + + RaisePropertyChanged("Stroke"); + } + } + } + + /// + /// Gets or sets the line thickness. + /// + /// The line thickness. + 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; + } + + RaisePropertyChanged("StrokeThickness"); + } + } + } + + /// + /// Gets or sets the line pen. + /// + /// The line pen. + [NotNull] + public Pen LinePen + { + get { return (Pen)GetValue(LinePenProperty); } + set { SetValue(LinePenProperty, value); } + } + + public static readonly DependencyProperty LinePenProperty = + DependencyProperty.Register( + "LinePen", + typeof(Pen), + typeof(LineGraph), + new FrameworkPropertyMetadata( + new Pen(Brushes.Blue, 1), + FrameworkPropertyMetadataOptions.AffectsRender + ), + OnValidatePen); + + private static bool OnValidatePen(object value) + { + return value != null; + } + + #endregion + + protected override void OnOutputChanged(Rect newRect, Rect oldRect) + { + filteredPoints = null; + base.OnOutputChanged(newRect, oldRect); + } + + protected override void OnDataChanged() + { + filteredPoints = null; + base.OnDataChanged(); + } + + protected override void OnDataSourceChanged(DependencyPropertyChangedEventArgs args) + { + filteredPoints = null; + base.OnDataSourceChanged(args); + } + + protected override void OnVisibleChanged(DataRect newRect, DataRect oldRect) + { + if (newRect.Size != oldRect.Size) + { + filteredPoints = null; + } + + base.OnVisibleChanged(newRect, oldRect); + } + + private FakePointList filteredPoints; + protected FakePointList FilteredPoints + { + get { return filteredPoints; } + set { filteredPoints = value; } + } + + protected override void UpdateCore() + { + if (DataSource == null) return; + if (Plotter == null) return; + + Rect output = Viewport.Output; + var transform = GetTransform(); + + if (filteredPoints == null || !(transform.DataTransform is IdentityTransform)) + { + IEnumerable points = GetPoints(); + + var bounds = BoundsHelper.GetViewportBounds(points, transform.DataTransform); + Viewport2D.SetContentBounds(this, bounds); + + // getting new value of transform as it could change after calculating and setting content bounds. + transform = GetTransform(); + List transformedPoints = transform.DataToScreenAsList(points); + + // Analysis and filtering of unnecessary points + filteredPoints = new FakePointList(FilterPoints(transformedPoints), + output.Left, output.Right); + + if (ProvideVisiblePoints) + { + List viewportPointsList = new List(transformedPoints.Count); + if (transform.DataTransform is IdentityTransform) + { + viewportPointsList.AddRange(points); + } + else + { + var viewportPoints = points.DataToViewport(transform.DataTransform); + viewportPointsList.AddRange(viewportPoints); + } + + SetVisiblePoints(this, new ReadOnlyCollection(viewportPointsList)); + } + + Offset = new Vector(); + } + else + { + double left = output.Left; + double right = output.Right; + double shift = Offset.X; + left -= shift; + right -= shift; + + filteredPoints.SetXBorders(left, right); + } + } + + StreamGeometry streamGeometry = new StreamGeometry(); + protected override void OnRenderCore(DrawingContext dc, RenderState state) + { + if (DataSource == null) return; + + if (filteredPoints.HasPoints) + { + + using (StreamGeometryContext context = streamGeometry.Open()) + { + context.BeginFigure(filteredPoints.StartPoint, false, false); + context.PolyLineTo(filteredPoints, true, smoothLinesJoin); + } + + Brush brush = null; + Pen pen = LinePen; + + bool isTranslated = IsTranslated; + if (isTranslated) + { + dc.PushTransform(new TranslateTransform(Offset.X, Offset.Y)); + } + dc.DrawGeometry(brush, pen, streamGeometry); + if (isTranslated) + { + dc.Pop(); + } + +#if __DEBUG + FormattedText text = new FormattedText(filteredPoints.Count.ToString(), + CultureInfo.InvariantCulture, FlowDirection.LeftToRight, + new Typeface("Arial"), 12, Brushes.Black); + dc.DrawText(text, Viewport.Output.GetCenter()); +#endif + } + } + + private bool filteringEnabled = true; + public bool FilteringEnabled + { + get { return filteringEnabled; } + set + { + if (filteringEnabled != value) + { + filteringEnabled = value; + filteredPoints = null; + Update(); + } + } + } + + private bool smoothLinesJoin = true; + public bool SmoothLinesJoin + { + get { return smoothLinesJoin; } + set + { + smoothLinesJoin = value; + Update(); + } + } + + private List FilterPoints(List points) + { + if (!filteringEnabled) + return points; + + var filteredPoints = filters.Filter(points, Viewport.Output); + + return filteredPoints; + } + } +} diff --git a/Charts/LineLegendItem.xaml b/Charts/LineLegendItem.xaml new file mode 100644 index 0000000..5107531 --- /dev/null +++ b/Charts/LineLegendItem.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/Charts/LineLegendItem.xaml.cs b/Charts/LineLegendItem.xaml.cs new file mode 100644 index 0000000..bb33b04 --- /dev/null +++ b/Charts/LineLegendItem.xaml.cs @@ -0,0 +1,18 @@ +namespace Microsoft.Research.DynamicDataDisplay +{ + /// + /// Interaction logic for LineLegendItem.xaml + /// + public partial class LineLegendItem : LegendItem + { + public LineLegendItem() + { + InitializeComponent(); + } + + public LineLegendItem(Description description) : base(description) + { + InitializeComponent(); + } + } +} diff --git a/Charts/LiveTooltips/LiveTooltip.cs b/Charts/LiveTooltips/LiveTooltip.cs new file mode 100644 index 0000000..4612714 --- /dev/null +++ b/Charts/LiveTooltips/LiveTooltip.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows; +using System.Windows.Media; +using System.Windows.Input; +using System.Diagnostics; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class LiveToolTip : ContentControl + { + static int nameCounter = 0; + static LiveToolTip() + { + var thisType = typeof(LiveToolTip); + + DefaultStyleKeyProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(thisType)); + FocusableProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(false)); + IsHitTestVisibleProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(false)); + BackgroundProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(Brushes.White)); + OpacityProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(1.0)); + BorderBrushProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(Brushes.DarkGray)); + BorderThicknessProperty.OverrideMetadata(thisType, new FrameworkPropertyMetadata(new Thickness(1.0))); + } + + public LiveToolTip() + { + Name = "Microsoft_Research_Dynamic_Data_Display_Charts_LiveToolTip_" + nameCounter; + nameCounter++; + } + + #region Properties + + public FrameworkElement Owner + { + get { return (FrameworkElement)GetValue(OwnerProperty); } + set { SetValue(OwnerProperty, value); } + } + + public static readonly DependencyProperty OwnerProperty = DependencyProperty.Register( + "Owner", + typeof(FrameworkElement), + typeof(LiveToolTip), + new FrameworkPropertyMetadata(null)); + + #endregion // end of Properties + } +} diff --git a/Charts/LiveTooltips/LiveTooltipAdorner.cs b/Charts/LiveTooltips/LiveTooltipAdorner.cs new file mode 100644 index 0000000..7aec78a --- /dev/null +++ b/Charts/LiveTooltips/LiveTooltipAdorner.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Documents; +using System.Windows; +using System.Windows.Media; +using System.Windows.Input; +using System.Windows.Media.Effects; +using System.Windows.Controls; +using System.Windows.Shapes; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class LiveToolTipAdorner : Adorner + { + private Canvas canvas = new Canvas { IsHitTestVisible = false }; + private readonly VisualCollection visualChildren; + public LiveToolTipAdorner(UIElement adornedElement, LiveToolTip tooltip) + : base(adornedElement) + { + visualChildren = new VisualCollection(this); + + adornedElement.MouseLeave += adornedElement_MouseLeave; + adornedElement.MouseEnter += adornedElement_MouseEnter; + adornedElement.PreviewMouseMove += adornedElement_MouseMove; + //FrameworkElement frAdornedElement = (FrameworkElement)adornedElement; + //frAdornedElement.SizeChanged += frAdornedElement_SizeChanged; + + this.liveTooltip = tooltip; + + tooltip.Visibility = Visibility.Hidden; + + canvas.Children.Add(liveTooltip); + AddLogicalChild(canvas); + visualChildren.Add(canvas); + + Unloaded += LiveTooltipAdorner_Unloaded; + } + + //void frAdornedElement_SizeChanged(object sender, SizeChangedEventArgs e) + //{ + // grid.Width = e.NewSize.Width; + // grid.Height = e.NewSize.Height; + + // InvalidateMeasure(); + //} + + void LiveTooltipAdorner_Unloaded(object sender, RoutedEventArgs e) + { + canvas.Children.Remove(liveTooltip); + } + + void adornedElement_MouseLeave(object sender, MouseEventArgs e) + { + liveTooltip.Visibility = Visibility.Hidden; + } + + void adornedElement_MouseEnter(object sender, MouseEventArgs e) + { + liveTooltip.Visibility = Visibility.Visible; + } + + Point mousePosition; + private void adornedElement_MouseMove(object sender, MouseEventArgs e) + { + liveTooltip.Visibility = Visibility.Visible; + mousePosition = e.GetPosition(AdornedElement); + InvalidateMeasure(); + } + + private void ArrangeTooltip() + { + Size tooltipSize = liveTooltip.DesiredSize; + + Point location = mousePosition; + location.Offset(-tooltipSize.Width / 2, -tooltipSize.Height - 1); + + liveTooltip.Arrange(new Rect(location, tooltipSize)); + } + + LiveToolTip liveTooltip; + public LiveToolTip LiveTooltip + { + get { return liveTooltip; } + } + + #region Overrides + + protected override Visual GetVisualChild(int index) + { + return visualChildren[index]; + } + + protected override int VisualChildrenCount + { + get { return visualChildren.Count; } + } + + protected override Size MeasureOverride(Size constraint) + { + foreach (UIElement item in visualChildren) + { + item.Measure(constraint); + } + + liveTooltip.Measure(constraint); + + return base.MeasureOverride(constraint); + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (UIElement item in visualChildren) + { + item.Arrange(new Rect(item.DesiredSize)); + } + + ArrangeTooltip(); + + return finalSize; + } + + #endregion // end of overrides + } +} diff --git a/Charts/LiveTooltips/LiveTooltipService.cs b/Charts/LiveTooltips/LiveTooltipService.cs new file mode 100644 index 0000000..92f4507 --- /dev/null +++ b/Charts/LiveTooltips/LiveTooltipService.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Input; +using System.Windows.Documents; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Data; +using System.ComponentModel; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public static class LiveToolTipService + { + + # region Properties + + public static object GetToolTip(DependencyObject obj) + { + return (object)obj.GetValue(ToolTipProperty); + } + + public static void SetToolTip(DependencyObject obj, object value) + { + obj.SetValue(ToolTipProperty, value); + } + + public static readonly DependencyProperty ToolTipProperty = DependencyProperty.RegisterAttached( + "ToolTip", + typeof(object), + typeof(LiveToolTipService), + new FrameworkPropertyMetadata(null, OnToolTipChanged)); + + private static LiveToolTip GetLiveToolTip(DependencyObject obj) + { + return (LiveToolTip)obj.GetValue(LiveToolTipProperty); + } + + private static void SetLiveToolTip(DependencyObject obj, LiveToolTip value) + { + obj.SetValue(LiveToolTipProperty, value); + } + + private static readonly DependencyProperty LiveToolTipProperty = DependencyProperty.RegisterAttached( + "LiveToolTip", + typeof(LiveToolTip), + typeof(LiveToolTipService), + new FrameworkPropertyMetadata(null)); + + #region Opacity + + public static double GetTooltipOpacity(DependencyObject obj) + { + return (double)obj.GetValue(TooltipOpacityProperty); + } + + public static void SetTooltipOpacity(DependencyObject obj, double value) + { + obj.SetValue(TooltipOpacityProperty, value); + } + + public static readonly DependencyProperty TooltipOpacityProperty = DependencyProperty.RegisterAttached( + "TooltipOpacity", + typeof(double), + typeof(LiveToolTipService), + new FrameworkPropertyMetadata(1.0, OnTooltipOpacityChanged)); + + private static void OnTooltipOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + LiveToolTip liveTooltip = GetLiveToolTip(d); + if (liveTooltip != null) + { + liveTooltip.Opacity = (double)e.NewValue; + } + } + + #endregion // end of Opacity + + #region IsPropertyProxy property + + public static bool GetIsPropertyProxy(DependencyObject obj) + { + return (bool)obj.GetValue(IsPropertyProxyProperty); + } + + public static void SetIsPropertyProxy(DependencyObject obj, bool value) + { + obj.SetValue(IsPropertyProxyProperty, value); + } + + public static readonly DependencyProperty IsPropertyProxyProperty = DependencyProperty.RegisterAttached( + "IsPropertyProxy", + typeof(bool), + typeof(LiveToolTipService), + new FrameworkPropertyMetadata(false)); + + #endregion // end of IsPropertyProxy property + + #endregion + + private static void OnToolTipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + FrameworkElement source = (FrameworkElement)d; + + if (e.NewValue == null) + { + source.Loaded -= source_Loaded; + source.ClearValue(LiveToolTipProperty); + } + + if (GetIsPropertyProxy(source)) return; + + var content = e.NewValue; + + DataTemplate template = content as DataTemplate; + if (template != null) + { + content = template.LoadContent(); + } + + LiveToolTip tooltip = null; + if (e.NewValue is LiveToolTip) + { + tooltip = e.NewValue as LiveToolTip; + } + else + { + tooltip = new LiveToolTip { Content = content }; + } + + if (tooltip == null && e.OldValue == null) + { + tooltip = new LiveToolTip { Content = content }; + } + + if (tooltip != null) + { + SetLiveToolTip(source, tooltip); + if (!source.IsLoaded) + { + source.Loaded += source_Loaded; + } + else + { + AddTooltip(source); + } + } + } + + private static void AddTooltipForElement(FrameworkElement source, LiveToolTip tooltip) + { + AdornerLayer layer = AdornerLayer.GetAdornerLayer(source); + + LiveToolTipAdorner adorner = new LiveToolTipAdorner(source, tooltip); + layer.Add(adorner); + } + + private static void source_Loaded(object sender, RoutedEventArgs e) + { + FrameworkElement source = (FrameworkElement)sender; + + if (source.IsLoaded) + { + AddTooltip(source); + } + } + + private static void AddTooltip(FrameworkElement source) + { + if (DesignerProperties.GetIsInDesignMode(source)) return; + + LiveToolTip tooltip = GetLiveToolTip(source); + + Window window = Window.GetWindow(source); + FrameworkElement child = source; + FrameworkElement parent = null; + if (window != null) + { + while (parent != window) + { + parent = (FrameworkElement)VisualTreeHelper.GetParent(child); + child = parent; + var nameScope = NameScope.GetNameScope(parent); + if (nameScope != null) + { + string nameScopeName = nameScope.ToString(); + if (nameScopeName != "System.Windows.TemplateNameScope") + { + NameScope.SetNameScope(tooltip, nameScope); + break; + } + } + } + } + + var binding = BindingOperations.GetBinding(tooltip, LiveToolTip.ContentProperty); + if (binding != null) + { + BindingOperations.ClearBinding(tooltip, LiveToolTip.ContentProperty); + BindingOperations.SetBinding(tooltip, LiveToolTip.ContentProperty, binding); + } + + Binding dataContextBinding = new Binding { Path = new PropertyPath("DataContext"), Source = source }; + tooltip.SetBinding(LiveToolTip.DataContextProperty, dataContextBinding); + + tooltip.Owner = source; + if (GetTooltipOpacity(source) != (double)LiveToolTipService.TooltipOpacityProperty.DefaultMetadata.DefaultValue) + { + tooltip.Opacity = LiveToolTipService.GetTooltipOpacity(source); + } + + AddTooltipForElement(source, tooltip); + } + } +} diff --git a/Charts/MagnifyingGlass.xaml b/Charts/MagnifyingGlass.xaml new file mode 100644 index 0000000..ab6d052 --- /dev/null +++ b/Charts/MagnifyingGlass.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/Charts/MagnifyingGlass.xaml.cs b/Charts/MagnifyingGlass.xaml.cs new file mode 100644 index 0000000..f6abd8c --- /dev/null +++ b/Charts/MagnifyingGlass.xaml.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Microsoft.Research.DynamicDataDisplay +{ + public partial class MagnifyingGlass : Grid, IPlotterElement + { + public MagnifyingGlass() + { + InitializeComponent(); + Loaded += MagnifyingGlass_Loaded; + + whiteEllipse.Visibility = Visibility.Collapsed; + magnifierEllipse.Visibility = Visibility.Collapsed; + } + + private void MagnifyingGlass_Loaded(object sender, RoutedEventArgs e) + { + UpdateViewbox(); + } + + protected override void OnPreviewMouseWheel(MouseWheelEventArgs e) + { + Magnification += e.Delta / Mouse.MouseWheelDeltaForOneLine * 0.2; + e.Handled = false; + } + + private void plotter_PreviewMouseMove(object sender, MouseEventArgs e) + { + VisualBrush b = (VisualBrush)magnifierEllipse.Fill; + Point pos = e.GetPosition(plotter.ParallelCanvas); + + Point plotterPos = e.GetPosition(plotter); + + Rect viewBox = b.Viewbox; + double xoffset = viewBox.Width / 2.0; + double yoffset = viewBox.Height / 2.0; + viewBox.X = plotterPos.X - xoffset; + viewBox.Y = plotterPos.Y - yoffset; + b.Viewbox = viewBox; + Canvas.SetLeft(this, pos.X - Width / 2); + Canvas.SetTop(this, pos.Y - Height / 2); + } + + private double magnification = 2.0; + public double Magnification + { + get { return magnification; } + set + { + magnification = value; + + UpdateViewbox(); + } + } + + private void UpdateViewbox() + { + if (!IsLoaded) + return; + + VisualBrush b = (VisualBrush)magnifierEllipse.Fill; + Rect viewBox = b.Viewbox; + viewBox.Width = Width / magnification; + viewBox.Height = Height / magnification; + b.Viewbox = viewBox; + } + + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == WidthProperty || e.Property == HeightProperty) + { + UpdateViewbox(); + } + } + + #region IPlotterElement Members + + Plotter plotter; + public void OnPlotterAttached(Plotter plotter) + { + this.plotter = plotter; + plotter.ParallelCanvas.Children.Add(this); + plotter.PreviewMouseMove += plotter_PreviewMouseMove; + plotter.MouseEnter += new MouseEventHandler(plotter_MouseEnter); + plotter.MouseLeave += new MouseEventHandler(plotter_MouseLeave); + + VisualBrush b = (VisualBrush)magnifierEllipse.Fill; + b.Visual = plotter.MainGrid; + } + + void plotter_MouseLeave(object sender, MouseEventArgs e) + { + whiteEllipse.Visibility = Visibility.Collapsed; + magnifierEllipse.Visibility = Visibility.Collapsed; + } + + void plotter_MouseEnter(object sender, MouseEventArgs e) + { + whiteEllipse.Visibility = Visibility.Visible; + magnifierEllipse.Visibility = Visibility.Visible; + } + + public void OnPlotterDetaching(Plotter plotter) + { + plotter.MouseEnter -= new MouseEventHandler(plotter_MouseEnter); + plotter.MouseLeave -= new MouseEventHandler(plotter_MouseLeave); + + plotter.PreviewMouseMove -= plotter_PreviewMouseMove; + plotter.ParallelCanvas.Children.Remove(this); + this.plotter = null; + + VisualBrush b = (VisualBrush)magnifierEllipse.Fill; + b.Visual = null; + } + + public Plotter Plotter + { + get { return plotter; ; } + } + + #endregion + } +} diff --git a/Charts/MarkerElementPointGraph.cs b/Charts/MarkerElementPointGraph.cs new file mode 100644 index 0000000..0bb69d5 --- /dev/null +++ b/Charts/MarkerElementPointGraph.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; +using Microsoft.Research.DynamicDataDisplay.DataSources; +using Microsoft.Research.DynamicDataDisplay.PointMarkers; +using Microsoft.Research.DynamicDataDisplay.Common; +using System.Windows.Controls; + +namespace Microsoft.Research.DynamicDataDisplay +{ + public class ElementMarkerPointsGraph : PointsGraphBase + { + /// List with created but unused markers + private readonly List unused = new List(); + + /// Initializes a new instance of the class. + public ElementMarkerPointsGraph() + { + ManualTranslate = true; // We'll handle translation by ourselves + } + + /// Initializes a new instance of the class. + /// The data source. + public ElementMarkerPointsGraph(IPointDataSource dataSource) + : this() + { + DataSource = dataSource; + } + + Grid grid; + Canvas canvas; + + protected override void OnPlotterAttached(Plotter plotter) + { + base.OnPlotterAttached(plotter); + + grid = new Grid(); + canvas = new Canvas { ClipToBounds = true }; + grid.Children.Add(canvas); + + Plotter2D.CentralGrid.Children.Add(grid); + } + + protected override void OnPlotterDetaching(Plotter plotter) + { + Plotter2D.CentralGrid.Children.Remove(grid); + grid = null; + canvas = null; + + base.OnPlotterDetaching(plotter); + } + + protected override void OnDataChanged() + { + // if (canvas != null) + // { + // foreach(UIElement child in canvas.Children) + // unused.Add(child); + // canvas.Children.Clear(); + // } + // todo почему так? + base.OnDataChanged(); + } + + public ElementPointMarker Marker + { + get { return (ElementPointMarker)GetValue(MarkerProperty); } + set { SetValue(MarkerProperty, value); } + } + + public static readonly DependencyProperty MarkerProperty = + DependencyProperty.Register( + "Marker", + typeof(ElementPointMarker), + typeof(ElementMarkerPointsGraph), + new FrameworkPropertyMetadata { DefaultValue = null, AffectsRender = true } + ); + + protected override void OnRenderCore(DrawingContext dc, RenderState state) + { + if (Marker == null) + return; + + if (DataSource == null) // No data is specified + { + if (canvas != null) + { + foreach (UIElement child in canvas.Children) + unused.Add(child); + canvas.Children.Clear(); + } + } + else // There is some data + { + + int index = 0; + var transform = GetTransform(); + using (IPointEnumerator enumerator = DataSource.GetEnumerator(GetContext())) + { + Point point = new Point(); + + DataRect bounds = DataRect.Empty; + + while (enumerator.MoveNext()) + { + enumerator.GetCurrent(ref point); + enumerator.ApplyMappings(Marker); + + if (index >= canvas.Children.Count) + { + UIElement newMarker; + if (unused.Count > 0) + { + newMarker = unused[unused.Count - 1]; + unused.RemoveAt(unused.Count - 1); + } + else + newMarker = Marker.CreateMarker(); + canvas.Children.Add(newMarker); + } + + Marker.SetMarkerProperties(canvas.Children[index]); + bounds.Union(point); + Point screenPoint = point.DataToScreen(transform); + Marker.SetPosition(canvas.Children[index], screenPoint); + index++; + } + + Viewport2D.SetContentBounds(this, bounds); + + while (index < canvas.Children.Count) + { + unused.Add(canvas.Children[index]); + canvas.Children.RemoveAt(index); + } + } + } + } + } +} \ No newline at end of file diff --git a/Charts/MarkerPointGraph.cs b/Charts/MarkerPointGraph.cs new file mode 100644 index 0000000..a967375 --- /dev/null +++ b/Charts/MarkerPointGraph.cs @@ -0,0 +1,62 @@ +using System.Windows; +using System.Windows.Media; +using Microsoft.Research.DynamicDataDisplay.DataSources; +using Microsoft.Research.DynamicDataDisplay.PointMarkers; + +namespace Microsoft.Research.DynamicDataDisplay +{ + public class MarkerPointsGraph : PointsGraphBase + { + /// + /// Initializes a new instance of the class. + /// + public MarkerPointsGraph() { } + + /// + /// Initializes a new instance of the class. + /// + /// The data source. + public MarkerPointsGraph(IPointDataSource dataSource) + { + DataSource = dataSource; + } + + public PointMarker Marker + { + get { return (PointMarker)GetValue(MarkerProperty); } + set { SetValue(MarkerProperty, value); } + } + + public static readonly DependencyProperty MarkerProperty = + DependencyProperty.Register( + "Marker", + typeof(PointMarker), + typeof(MarkerPointsGraph), + new FrameworkPropertyMetadata { DefaultValue = null, AffectsRender = true } + ); + + protected override void OnRenderCore(DrawingContext dc, RenderState state) + { + if (DataSource == null) return; + if (Marker == null) return; + + Rect bounds = Rect.Empty; + using (IPointEnumerator enumerator = DataSource.GetEnumerator(GetContext())) + { + Point point = new Point(); + while (enumerator.MoveNext()) + { + enumerator.GetCurrent(ref point); + enumerator.ApplyMappings(Marker); + + Point screenPoint = point.Transform(state.Visible, state.Output); + + bounds = Rect.Union(bounds, point); + Marker.Render(dc, screenPoint); + } + } + + ContentBounds = bounds; + } + } +} diff --git a/Charts/MarkerPointsGraph.cs b/Charts/MarkerPointsGraph.cs new file mode 100644 index 0000000..c587c68 --- /dev/null +++ b/Charts/MarkerPointsGraph.cs @@ -0,0 +1,76 @@ +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 +{ + public class MarkerPointsGraph : PointsGraphBase + { + /// + /// Initializes a new instance of the class. + /// + public MarkerPointsGraph() + { + ManualTranslate = true; + } + + /// + /// Initializes a new instance of the class. + /// + /// The data source. + public MarkerPointsGraph(IPointDataSource dataSource) + : this() + { + DataSource = dataSource; + } + + protected override void OnVisibleChanged(DataRect newRect, DataRect oldRect) + { + base.OnVisibleChanged(newRect, oldRect); + InvalidateVisual(); + } + + public PointMarker Marker + { + get { return (PointMarker)GetValue(MarkerProperty); } + set { SetValue(MarkerProperty, value); } + } + + public static readonly DependencyProperty MarkerProperty = + DependencyProperty.Register( + "Marker", + typeof(PointMarker), + typeof(MarkerPointsGraph), + new FrameworkPropertyMetadata { DefaultValue = null, AffectsRender = true } + ); + + protected override void OnRenderCore(DrawingContext dc, RenderState state) + { + if (DataSource == null) return; + if (Marker == null) return; + + 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); + enumerator.ApplyMappings(Marker); + + //Point screenPoint = point.Transform(state.Visible, state.Output); + Point screenPoint = point.DataToScreen(transform); + + bounds = DataRect.Union(bounds, point); + Marker.Render(dc, screenPoint); + } + } + + Viewport2D.SetContentBounds(this, bounds); + } + } +} diff --git a/Charts/Markers/BarChart.cs b/Charts/Markers/BarChart.cs new file mode 100644 index 0000000..5aed786 --- /dev/null +++ b/Charts/Markers/BarChart.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Data; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Markers +{ + public class OldBarChart : MarkerChart + { + /// + /// Initializes a new instance of the class. + /// + public OldBarChart() { } + + protected override void OnDataSourceChanged() + { + } + + BarFromValueConverter converter = new BarFromValueConverter(); + List xValues = new List(); + protected override void OnMarkerBind(BindMarkerEventArgs e) + { + var marker = e.Marker; + + //marker.SetBinding(ViewportRectPanel.ViewportVerticalAlignmentProperty, new Binding { Path = new PropertyPath("Value"), Converter = converter }); + + xValues.Add(ViewportPanel.GetX(marker)); + } + + protected override void RebuildMarkers(bool shouldReleaseMarkers) + { + xValues.Clear(); + + base.RebuildMarkers(shouldReleaseMarkers); + + double width = 0; + for (int i = 0; i < xValues.Count - 1; i++) + { + double currX = xValues[i]; + double nextX = xValues[i + 1]; + width = (nextX - currX); + + ViewportPanel.SetViewportWidth(ItemsPanel.Children[i], width); + } + + if (ItemsPanel.Children.Count > 0) + { + ViewportPanel.SetViewportWidth(ItemsPanel.Children[ItemsPanel.Children.Count - 1], width); + } + } + } +} diff --git a/Charts/Markers/BarFromValueConverter.cs b/Charts/Markers/BarFromValueConverter.cs new file mode 100644 index 0000000..f1e65c1 --- /dev/null +++ b/Charts/Markers/BarFromValueConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Data; +using System.Globalization; +using System.Windows; +using System.Windows.Media; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Markers +{ + public class BarFromValueConverter : IValueConverter + { + /// + /// Initializes a new instance of the class. + /// + public BarFromValueConverter() { } + + public Brush PositiveBrush { get; set; } + public Brush NegativeBrush { get; set; } + + #region IValueConverter Members + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!(value is double)) + return DependencyProperty.UnsetValue; + + double height = (double)value; + + if (targetType == typeof(Brush)) + { + if (height > 0) + return PositiveBrush; + else + return NegativeBrush; + } + else if (targetType == typeof(double)) + { + return Math.Abs(height); + } + else if (targetType == typeof(VerticalAlignment)) + { + return height > 0 ? VerticalAlignment.Bottom : VerticalAlignment.Top; + } + + return DependencyProperty.UnsetValue; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + #endregion + } +} diff --git a/Charts/Markers/BindMarkerInfo.cs b/Charts/Markers/BindMarkerInfo.cs new file mode 100644 index 0000000..21272b7 --- /dev/null +++ b/Charts/Markers/BindMarkerInfo.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Markers +{ + public sealed class BindMarkerEventArgs + { + internal BindMarkerEventArgs() { } + + public FrameworkElement Marker { get; internal set; } + public object Data { get; internal set; } + } +} diff --git a/Charts/Markers/MarkerChart.cs b/Charts/Markers/MarkerChart.cs new file mode 100644 index 0000000..fb2793e --- /dev/null +++ b/Charts/Markers/MarkerChart.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Controls; +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.Research.DynamicDataDisplay.Common; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Markers +{ + public class MarkerChart : PlotterElement + { + /// + /// Initializes a new instance of the class. + /// + public MarkerChart() + { + Viewport2D.SetIsContentBoundsHost(this, true); + } + + #region DataSource + + private IEnumerable dataSource; + /// + /// Gets or sets the data source. + /// Value can be null. + /// + /// The data source. + public IEnumerable DataSource + { + get { return dataSource; } + set + { + DetachDataSource(); + dataSource = value; + AttachDataSource(); + + OnDataSourceChanged(); + + RebuildMarkers(true); + } + } + + protected virtual void OnDataSourceChanged() { } + + protected virtual void RebuildMarkers(bool shouldReleaseMarkers) + { + if (markerGenerator == null) + return; + + if (shouldReleaseMarkers) + { + foreach (FrameworkElement item in itemsPanel.Children) + { + var enumerator = item.GetLocalValueEnumerator(); + while (enumerator.MoveNext()) + { + item.ClearValue(enumerator.Current.Property); + } + + descriptor.RemoveValueChanged(item, OnMarkerViewportBoundsChanged); + markerGenerator.ReleaseMarker(item); + } + } + + itemsPanel.Children.Clear(); + + if (dataSource == null) + return; + + IndividualArrangePanel specialPanel = itemsPanel as IndividualArrangePanel; + if (specialPanel != null) + specialPanel.BeginBatchAdd(); + + foreach (object item in dataSource) + { + var marker = CreateMarker(item); + itemsPanel.Children.Add(marker); + } + + if (specialPanel != null) + specialPanel.EndBatchAdd(); + + RecalculateViewportBounds(); + } + + private FrameworkElement CreateMarker(object item) + { + var marker = markerGenerator.CreateMarker(item); + if (marker != null) + { + marker.DataContext = item; + AttachViewportChangedListener(marker); + + BindMarkerEventArgs bindArgs = new BindMarkerEventArgs { Data = item, Marker = marker }; + + OnMarkerBind(bindArgs); + + if (markerBindCallback != null) + { + markerBindCallback(bindArgs); + } + } + + return marker; + } + + protected virtual void OnMarkerBind(BindMarkerEventArgs e) { } + + private Action markerBindCallback = null; + public Action MarkerBindCallback + { + get { return markerBindCallback; } + set + { + if (markerBindCallback != value) + { + markerBindCallback = value; + RebuildMarkers(false); + } + } + } + + private void AttachDataSource() + { + INotifyCollectionChanged notifyingCollection = dataSource as INotifyCollectionChanged; + if (notifyingCollection != null) + { + notifyingCollection.CollectionChanged += OnDataSourceCollectionChanged; + } + } + + private void OnDataSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + int index = e.OldStartingIndex; + foreach (object item in e.OldItems) + { + var oldMarker = itemsPanel.Children[index]; + + if (markerGenerator != null) + { + FrameworkElement element = (FrameworkElement)oldMarker; + // todo call cleanup callback here + markerGenerator.ReleaseMarker(element); + descriptor.RemoveValueChanged(element, OnMarkerViewportBoundsChanged); + } + + itemsPanel.Children.RemoveAt(index); + } + } + + if (e.NewItems != null) + { + int index = e.NewStartingIndex; + foreach (object item in e.NewItems) + { + var marker = CreateMarker(item); + itemsPanel.Children.Insert(index, marker); + index++; + } + } + + RecalculateViewportBounds(); + } + + DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(ViewportPanel.ActualViewportBoundsProperty, typeof(FrameworkElement)); + + private void AttachViewportChangedListener(FrameworkElement element) + { + // todo or use this, or remove this + + //descriptor.RemoveValueChanged(element, OnMarkerViewportBoundsChanged); + //descriptor.AddValueChanged(element, OnMarkerViewportBoundsChanged); + } + + private Rect viewportBounds = Rect.Empty; + + private void OnMarkerViewportBoundsChanged(object sender, EventArgs e) + { + RecalculateViewportBounds(); + FrameworkElement element = (FrameworkElement)sender; + DataRect elementBounds = ViewportPanel.GetActualViewportBounds(element); + DataRect prevElementBounds = ViewportPanel.GetPrevActualViewportBounds(element); + } + + private void RecalculateViewportBounds() + { + DataRect bounds = DataRect.Empty; + + foreach (UIElement item in itemsPanel.Children) + { + DataRect elementBounds = ViewportPanel.GetActualViewportBounds(item); + bounds.Union(elementBounds); + } + + Viewport2D.SetContentBounds(this, bounds); + } + + private void DetachDataSource() + { + INotifyCollectionChanged notifyingCollection = dataSource as INotifyCollectionChanged; + if (notifyingCollection != null) + { + notifyingCollection.CollectionChanged -= OnDataSourceCollectionChanged; + } + } + + #endregion + + #region Marker + + private OldMarkerGenerator markerGenerator; + public OldMarkerGenerator MarkerGenerator + { + get { return markerGenerator; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (markerGenerator != value) + { + DetachMarkerGenerator(); + markerGenerator = value; + AttachMarkerGenerator(); + + RebuildMarkers(false); + } + } + } + + private void AttachMarkerGenerator() + { + markerGenerator.Changed += OnMarkerChanged; + } + + private void OnMarkerChanged(object sender, EventArgs e) + { + RebuildMarkers(false); + } + + private void DetachMarkerGenerator() + { + if (markerGenerator != null) + { + markerGenerator.Changed -= OnMarkerChanged; + } + } + + #endregion + + #region Plotter attaching + + private ViewportPanel itemsPanel = new ViewportPanel(); + protected Panel ItemsPanel + { + get { return itemsPanel; } + } + + private Plotter2D plotter; + protected override void OnPlotterAttached(Plotter plotter) + { + base.OnPlotterAttached(plotter); + + this.plotter = (Plotter2D)plotter; + ((IPlotterElement)itemsPanel).OnPlotterAttached(plotter); + } + + protected override void OnPlotterDetaching(Plotter plotter) + { + ((IPlotterElement)itemsPanel).OnPlotterDetaching(plotter); + + this.plotter = null; + + base.OnPlotterDetaching(plotter); + } + + #endregion + } +} diff --git a/Charts/Markers/OldMarkerGenerator.cs b/Charts/Markers/OldMarkerGenerator.cs new file mode 100644 index 0000000..23cbf92 --- /dev/null +++ b/Charts/Markers/OldMarkerGenerator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Markers +{ + public abstract class OldMarkerGenerator : DependencyObject + { + public FrameworkElement CreateMarker(object dataItem) + { + return CreateMarkerCore(dataItem); + } + + protected abstract FrameworkElement CreateMarkerCore(object dataItem); + + protected void RaiseChanged() + { + Changed.Raise(this); + } + public event EventHandler Changed; + + public virtual void ReleaseMarker(FrameworkElement element) { } + } +} diff --git a/Charts/Markers/TemplateMarkerGenerator2.cs b/Charts/Markers/TemplateMarkerGenerator2.cs new file mode 100644 index 0000000..b40c17a --- /dev/null +++ b/Charts/Markers/TemplateMarkerGenerator2.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using Microsoft.Research.DynamicDataDisplay.Common; +using System.Windows.Markup; +using System.Windows.Controls; + +namespace Microsoft.Research.DynamicDataDisplay.Charts.Markers +{ + [ContentProperty("Template")] + public class TemplateMarkerGenerator2 : OldMarkerGenerator + { + private DataTemplate template; + [NotNull] + public DataTemplate Template + { + get { return template; } + set + { + if (value == null) + throw new ArgumentNullException("value"); + + if (template != value) + { + template = value; + pool.Clear(); + RaiseChanged(); + } + } + } + + private readonly ResourcePool pool = new ResourcePool(); + + protected override FrameworkElement CreateMarkerCore(object dataItem) + { + if (template == null) + throw new InvalidOperationException(Strings.Exceptions.TemplateShouldNotBeNull); + + FrameworkElement marker = pool.Get(); + if (marker == null) + { + marker = (FrameworkElement)template.LoadContent(); + } + + return marker; + } + + public override void ReleaseMarker(FrameworkElement element) + { + pool.Put(element); + } + } +} diff --git a/Charts/MarkersPointsGraph.cs b/Charts/MarkersPointsGraph.cs new file mode 100644 index 0000000..cc5d4eb --- /dev/null +++ b/Charts/MarkersPointsGraph.cs @@ -0,0 +1,78 @@ +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 +{ + public class MarkersPointsGraph : PointsGraphBase + { + /// + /// Initializes a new instance of the class. + /// + public MarkersPointsGraph() + { + ManualTranslate = true; + } + + /// + /// Initializes a new instance of the class. + /// + /// The data source. + public MarkersPointsGraph(IPointDataSource dataSource) + : this() + { + DataSource = dataSource; + } + + protected override void OnVisibleChanged(DataRect newRect, DataRect oldRect) + { + base.OnVisibleChanged(newRect, oldRect); + InvalidateVisual(); + } + + public PointMarker[] Markers + { + get { return (PointMarker[])GetValue(MarkerProperty); } + set { SetValue(MarkerProperty, value); } + } + + public static readonly DependencyProperty MarkerProperty = + DependencyProperty.Register( + "Markers", + typeof(PointMarker[]), + typeof(MarkersPointsGraph), + new FrameworkPropertyMetadata { DefaultValue = null, AffectsRender = true } + ); + + protected override void OnRenderCore(DrawingContext dc, RenderState state) + { + if (DataSource == null) return; + if (Markers == null) return; + + var transform = Plotter2D.Viewport.Transform; + + DataRect bounds = DataRect.Empty; + int index = 0; + using (IPointEnumerator enumerator = DataSource.GetEnumerator(GetContext())) + { + Point point = new Point(); + while (enumerator.MoveNext()) + { + enumerator.GetCurrent(ref point); + PointMarker pointMarker = null; + if (index >= Markers.Length) pointMarker=new CenteredTextMarker{Text=""}; + else pointMarker=Markers[index]; + enumerator.ApplyMappings(pointMarker); + Point screenPoint = point.DataToScreen(transform); + bounds = DataRect.Union(bounds, point); + pointMarker.Render(dc, screenPoint); + index++; + } + } + + Viewport2D.SetContentBounds(this, bounds); + } + } +} diff --git a/Charts/NaiveColorMap.cs b/Charts/NaiveColorMap.cs new file mode 100644 index 0000000..9862869 --- /dev/null +++ b/Charts/NaiveColorMap.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary; +using Microsoft.Research.DynamicDataDisplay.Common.Palettes; +using Microsoft.Research.DynamicDataDisplay.DataSources; + +namespace Microsoft.Research.DynamicDataDisplay.Charts +{ + public class NaiveColorMap + { + public double[,] Data { get; set; } + + public IPalette Palette { get; set; } + + public BitmapSource BuildImage() + { + if (Data == null) + throw new ArgumentNullException("Data"); + if (Palette == null) + throw new ArgumentNullException("Palette"); + + + int width = Data.GetLength(0); + int height = Data.GetLength(1); + + int[] pixels = new int[width * height]; + + var minMax = Data.GetMinMax(); + var min = minMax.Min; + var rangeDelta = minMax.GetLength(); + + int pointer = 0; + for (int iy = 0; iy < height; iy++) + { + for (int ix = 0; ix < width; ix++) + { + double value = Data[ix, height - 1 - iy]; + double ratio = (value - min) / rangeDelta; + Color color = Palette.GetColor(ratio); + int argb = color.ToArgb(); + + pixels[pointer++] = argb; + } + } + + WriteableBitmap bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null); + int bpp = (bitmap.Format.BitsPerPixel + 7) / 8; + int stride = bitmap.PixelWidth * bpp; + + bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0); + + return bitmap; + } + } +} diff --git a/Charts/Navigation/AboutWindow.xaml b/Charts/Navigation/AboutWindow.xaml new file mode 100644 index 0000000..dd9285c --- /dev/null +++ b/Charts/Navigation/AboutWindow.xaml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + Dynamic Data Display + + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +