commit 2bbedc017858a6eed00a99883b3b6accc50445e5 Author: Sean Kessler Date: Fri Feb 23 00:46:06 2024 -0500 Initial Commit 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 + + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +