Initial Commit

This commit is contained in:
2024-02-23 00:46:06 -05:00
commit 2bbedc0178
470 changed files with 46035 additions and 0 deletions

330
.gitignore vendored Normal file
View File

@@ -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/

42
AssemblyInfo.cs Normal file
View File

@@ -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";
}
}

1
Changelog.txt Normal file
View File

@@ -0,0 +1 @@
Renamed ChartPlotter's HorizontalAxis to MainHorizontalAxis, VerticalAxis to MainVerticalAxis.

503
ChartPlotter.cs Normal file
View File

@@ -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
{
/// <summary>Chart plotter is a plotter that renders axis and grid</summary>
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; }
}
/// <summary>
/// Initializes a new instance of the <see cref="ChartPlotter"/> class.
/// </summary>
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();
}
/// <summary>
/// Creates generic plotter from this ChartPlotter.
/// </summary>
/// <returns></returns>
public GenericChartPlotter<double, double> GetGenericPlotter()
{
return new GenericChartPlotter<double, double>(this);
}
/// <summary>
/// Creates generic plotter from this ChartPlotter.
/// Horizontal and Vertical types of GenericPlotter should correspond to ChartPlotter's actual main axes types.
/// </summary>
/// <typeparam name="THorizontal">The type of horizontal values.</typeparam>
/// <typeparam name="TVertical">The type of vertical values.</typeparam>
/// <returns>GenericChartPlotter, associated to this ChartPlotter.</returns>
public GenericChartPlotter<THorizontal, TVertical> GetGenericPlotter<THorizontal, TVertical>()
{
return new GenericChartPlotter<THorizontal, TVertical>(this);
}
/// <summary>
/// Creates generic plotter from this ChartPlotter.
/// </summary>
/// <typeparam name="THorizontal">The type of the horizontal axis.</typeparam>
/// <typeparam name="TVertical">The type of the vertical axis.</typeparam>
/// <param name="horizontalAxis">The horizontal axis to use as data conversion source.</param>
/// <param name="verticalAxis">The vertical axis to use as data conversion source.</param>
/// <returns>GenericChartPlotter, associated to this ChartPlotter</returns>
public GenericChartPlotter<THorizontal, TVertical> GetGenericPlotter<THorizontal, TVertical>(AxisBase<THorizontal> horizontalAxis, AxisBase<TVertical> verticalAxis)
{
return new GenericChartPlotter<THorizontal, TVertical>(this, horizontalAxis, verticalAxis);
}
protected ChartPlotter(PlotterLoadMode loadMode) : base(loadMode) { }
/// <summary>
/// Creates empty plotter without any axes, navigation, etc.
/// </summary>
/// <returns>Empty plotter without any axes, navigation, etc.</returns>
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;
/// <summary>
/// Gets the default mouse navigation of ChartPlotter.
/// </summary>
/// <value>The mouse navigation.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public MouseNavigation MouseNavigation
{
get { return mouseNavigation; }
}
private KeyboardNavigation keyboardNavigation;
/// <summary>
/// Gets the default keyboard navigation of ChartPlotter.
/// </summary>
/// <value>The keyboard navigation.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public KeyboardNavigation KeyboardNavigation
{
get { return keyboardNavigation; }
}
private DefaultContextMenu defaultContextMenu;
/// <summary>
/// Gets the default context menu of ChartPlotter.
/// </summary>
/// <value>The default context menu.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public DefaultContextMenu DefaultContextMenu
{
get { return defaultContextMenu; }
}
private AxisNavigation horizontalAxisNavigation;
/// <summary>
/// Gets the default horizontal axis navigation of ChartPlotter.
/// </summary>
/// <value>The horizontal axis navigation.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public AxisNavigation HorizontalAxisNavigation
{
get { return horizontalAxisNavigation; }
}
private AxisNavigation verticalAxisNavigation;
/// <summary>
/// Gets the default vertical axis navigation of ChartPlotter.
/// </summary>
/// <value>The vertical axis navigation.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public AxisNavigation VerticalAxisNavigation
{
get { return verticalAxisNavigation; }
}
/// <summary>
/// Gets the default axis grid of ChartPlotter.
/// </summary>
/// <value>The axis grid.</value>
[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;
/// <summary>
/// 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.
/// </summary>
/// <value>The main vertical axis.</value>
[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) { }
/// <summary>
/// Gets or sets the main horizontal axis visibility.
/// </summary>
/// <value>The main horizontal axis visibility.</value>
public Visibility MainHorizontalAxisVisibility
{
get { return MainHorizontalAxis != null ? ((UIElement)MainHorizontalAxis).Visibility : Visibility.Hidden; }
set
{
if (MainHorizontalAxis != null)
{
((UIElement)MainHorizontalAxis).Visibility = value;
}
}
}
/// <summary>
/// Gets or sets the main vertical axis visibility.
/// </summary>
/// <value>The main vertical axis visibility.</value>
public Visibility MainVerticalAxisVisibility
{
get { return MainVerticalAxis != null ? ((UIElement)MainVerticalAxis).Visibility : Visibility.Hidden; }
set
{
if (MainVerticalAxis != null)
{
((UIElement)MainVerticalAxis).Visibility = value;
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <value>The main horizontal axis.</value>
[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;
}
}
/// <summary>
/// Gets the default legend of ChartPlotter.
/// </summary>
/// <value>The legend.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Legend Legend
{
get { return legend; }
}
/// <summary>
/// Gets or sets the visibility of legend.
/// </summary>
/// <value>The legend visibility.</value>
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
}
}
}

411
Charts/Axes/AxisBase.cs Normal file
View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">Type of each tick's value</typeparam>
public abstract class AxisBase<T> : GeneralAxis, ITypedAxis<T>, IValueConversion<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="AxisBase&lt;T&gt;"/> class.
/// </summary>
/// <param name="axisControl">The axis control.</param>
/// <param name="convertFromDouble">The convert from double.</param>
/// <param name="convertToDouble">The convert to double.</param>
protected AxisBase(AxisControl<T> axisControl, Func<double, T> convertFromDouble, Func<T, double> 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();
}
/// <summary>
/// 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.
/// </summary>
/// <value>
/// <c>true</c> if this instance is default axis; otherwise, <c>false</c>.
/// </value>
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();
}
/// <summary>
/// Gets the screen coordinates of axis ticks.
/// </summary>
/// <value>The screen ticks.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override double[] ScreenTicks
{
get { return axisControl.ScreenTicks; }
}
/// <summary>
/// Gets the screen coordinates of minor ticks.
/// </summary>
/// <value>The minor screen ticks.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override MinorTickInfo<double>[] MinorScreenTicks
{
get { return axisControl.MinorScreenTicks; }
}
private AxisControl<T> axisControl;
/// <summary>
/// Gets the axis control - actual UI representation of axis.
/// </summary>
/// <value>The axis control.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public AxisControl<T> AxisControl
{
get { return axisControl; }
}
/// <summary>
/// Gets or sets the ticks provider, which is used to generate ticks in given range.
/// </summary>
/// <value>The ticks provider.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ITicksProvider<T> TicksProvider
{
get { return axisControl.TicksProvider; }
set { axisControl.TicksProvider = value; }
}
/// <summary>
/// Gets or sets the label provider, that is used to create UI look of axis ticks.
///
/// Should not be null.
/// </summary>
/// <value>The label provider.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[NotNull]
public LabelProviderBase<T> LabelProvider
{
get { return axisControl.LabelProvider; }
set { axisControl.LabelProvider = value; }
}
/// <summary>
/// Gets or sets the major label provider, which creates labels for major ticks.
/// If null, major labels will not be shown.
/// </summary>
/// <value>The major label provider.</value>
public LabelProviderBase<T> MajorLabelProvider
{
get { return axisControl.MajorLabelProvider; }
set { axisControl.MajorLabelProvider = value; }
}
/// <summary>
/// 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.
/// </summary>
/// <value>The label string format.</value>
public string LabelStringFormat
{
get { return LabelProvider.LabelStringFormat; }
set { LabelProvider.LabelStringFormat = value; }
}
/// <summary>
/// Gets or sets a value indicating whether to show minor ticks.
/// </summary>
/// <value><c>true</c> if show minor ticks; otherwise, <c>false</c>.</value>
public bool ShowMinorTicks
{
get { return axisControl.DrawMinorTicks; }
set { axisControl.DrawMinorTicks = value; }
}
/// <summary>
/// Gets or sets a value indicating whether to show major labels.
/// </summary>
/// <value><c>true</c> if show major labels; otherwise, <c>false</c>.</value>
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<T> 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<double, T> convertFromDouble;
/// <summary>
/// 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.
/// </summary>
/// <value>The convert from double.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[NotNull]
public Func<double, T> ConvertFromDouble
{
get { return convertFromDouble; }
set
{
if (value == null)
throw new ArgumentNullException("value");
if (convertFromDouble != value)
{
convertFromDouble = value;
if (ParentPlotter != null)
{
UpdateAxisControl(ParentPlotter);
}
}
}
}
private Func<T, double> convertToDouble;
/// <summary>
/// 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.
/// </summary>
/// <value>The convert to double.</value>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[NotNull]
public Func<T, double> ConvertToDouble
{
get { return convertToDouble; }
set
{
if (value == null)
throw new ArgumentNullException("value");
if (convertToDouble != value)
{
convertToDouble = value;
axisControl.ConvertToDouble = value;
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="min">The minimal viewport value.</param>
/// <param name="minValue">The value of axis type, corresponding to minimal viewport value.</param>
/// <param name="max">The maximal viewport value.</param>
/// <param name="maxValue">The value of axis type, corresponding to maximal viewport value.</param>
public virtual void SetConversion(double min, T minValue, double max, T maxValue)
{
throw new NotImplementedException();
}
private Range<T> CreateRangeFromRect(DataRect visible)
{
T min, max;
Range<T> 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<T>(min, max);
return range;
}
private static void TrySort<TS>(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;
}
}
}

1231
Charts/Axes/AxisControl.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}

View File

@@ -0,0 +1,76 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Microsoft.Research.DynamicDataDisplay.Charts"
>
<Style TargetType="{x:Type Grid}" x:Key="gridStyle">
<Setter Property="Grid.ClipToBounds" Value="False"/>
</Style>
<RotateTransform Angle="-90" x:Key="additionalLabelsTransformLeft"/>
<ControlTemplate x:Key="axisControlTemplateBottom">
<Grid Style="{StaticResource gridStyle}" Name="PART_ContentsGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Path Name="PART_TicksPath" Grid.Row="0" Stroke="Black"/>
<local:StackCanvas Name="PART_CommonLabelsCanvas" Grid.Row="1" Placement="Bottom"/>
<local:StackCanvas Name="PART_AdditionalLabelsCanvas" Grid.Row="2" Placement="Bottom"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="axisControlTemplateTop">
<Grid Background="{TemplateBinding Background}" Style="{StaticResource gridStyle}" Name="PART_ContentsGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Path Name="PART_TicksPath" Grid.Row="2" Stroke="Black">
<Path.LayoutTransform>
<MatrixTransform Matrix="1,0,0,-1.1,0,0"/>
</Path.LayoutTransform>
</Path>
<local:StackCanvas Name="PART_CommonLabelsCanvas" Grid.Row="1" Placement="Top"/>
<local:StackCanvas Name="PART_AdditionalLabelsCanvas" Grid.Row="0" Placement="Top"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="axisControlTemplateLeft">
<Grid Background="{TemplateBinding Background}" Style="{StaticResource gridStyle}" Name="PART_ContentsGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="2"/>
<Path Name="PART_TicksPath" Grid.Column="2" Stroke="Black">
<Path.LayoutTransform>
<MatrixTransform Matrix="-1,0,0,1,0,0"/>
</Path.LayoutTransform>
</Path>
<local:StackCanvas Name="PART_CommonLabelsCanvas" Grid.Column="1" Placement="Left" Margin="1,0,1,0"/>
<local:StackCanvas Name="PART_AdditionalLabelsCanvas" Grid.Column="0" Placement="Left" Margin="1,0,1,0"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="axisControlTemplateRight">
<Grid Background="{TemplateBinding Background}" Style="{StaticResource gridStyle}" Name="PART_ContentsGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Path Name="PART_TicksPath" Grid.Column="0" Stroke="Black"/>
<local:StackCanvas Name="PART_CommonLabelsCanvas" Grid.Column="1" Placement="Right" Margin="1,0,0,0"/>
<local:StackCanvas Name="PART_AdditionalLabelsCanvas" Grid.Column="2" Placement="Right"/>
</Grid>
</ControlTemplate>
</ResourceDictionary>

287
Charts/Axes/AxisGrid.cs Normal file
View File

@@ -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
{
/// <summary>
/// Draws grid over viewport. Number of
/// grid lines depends on Plotter's MainHorizontalAxis and MainVerticalAxis ticks.
/// </summary>
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<double>[] MinorHorizontalTicks { get; set; }
protected internal MinorTickInfo<double>[] MinorVerticalTicks { get; set; }
protected internal double[] HorizontalTicks { get; set; }
protected internal double[] VerticalTicks { get; set; }
private bool drawVerticalTicks = true;
/// <summary>
/// Gets or sets a value indicating whether to draw vertical tick lines.
/// </summary>
/// <value><c>true</c> if draw vertical ticks; otherwise, <c>false</c>.</value>
public bool DrawVerticalTicks
{
get { return drawVerticalTicks; }
set
{
if (drawVerticalTicks != value)
{
drawVerticalTicks = value;
UpdateUIRepresentation();
}
}
}
private bool drawHorizontalTicks = true;
/// <summary>
/// Gets or sets a value indicating whether to draw horizontal tick lines.
/// </summary>
/// <value><c>true</c> if draw horizontal ticks; otherwise, <c>false</c>.</value>
public bool DrawHorizontalTicks
{
get { return drawHorizontalTicks; }
set
{
if (drawHorizontalTicks != value)
{
drawHorizontalTicks = value;
UpdateUIRepresentation();
}
}
}
private bool drawHorizontalMinorTicks = false;
/// <summary>
/// Gets or sets a value indicating whether to draw horizontal minor ticks.
/// </summary>
/// <value>
/// <c>true</c> if draw horizontal minor ticks; otherwise, <c>false</c>.
/// </value>
public bool DrawHorizontalMinorTicks
{
get { return drawHorizontalMinorTicks; }
set
{
if (drawHorizontalMinorTicks != value)
{
drawHorizontalMinorTicks = value;
UpdateUIRepresentation();
}
}
}
private bool drawVerticalMinorTicks = false;
/// <summary>
/// Gets or sets a value indicating whether to draw vertical minor ticks.
/// </summary>
/// <value>
/// <c>true</c> if draw vertical minor ticks; otherwise, <c>false</c>.
/// </value>
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();
/// <summary>
/// Initializes a new instance of the <see cref="AxisGrid"/> class.
/// </summary>
public AxisGrid()
{
IsHitTestVisible = false;
canvas.ClipToBounds = true;
path.Stroke = Brushes.LightGray;
path.StrokeThickness = gridBrushThickness;
Content = canvas;
}
private readonly ResourcePool<LineGeometry> lineGeometryPool = new ResourcePool<LineGeometry>();
private readonly ResourcePool<Line> linePool = new ResourcePool<Line>();
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
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// Defines the position of axis inside ChartPlotter.
/// </summary>
public enum AxisPlacement
{
/// <summary>
/// Axis is placed to the left.
/// </summary>
Left,
/// <summary>
/// Axis is placed to the right.
/// </summary>
Right,
/// <summary>
/// Axis is placed to the top.
/// </summary>
Top,
/// <summary>
/// Axis is placed to the bottom.
/// </summary>
Bottom
}
}

View File

@@ -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
{
/// <summary>
/// Represents an axis with ticks of <see cref="System.DateTime"/> type.
/// </summary>
public class DateTimeAxis : AxisBase<DateTime>
{
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeAxis"/> class.
/// </summary>
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
/// <summary>
/// Gets or sets the major tick labels' background brush. This is a DependencyProperty.
/// </summary>
/// <value>The major label background brush.</value>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="min">The minimal viewport value.</param>
/// <param name="minValue">The value of axis type, corresponding to minimal viewport value.</param>
/// <param name="max">The maximal viewport value.</param>
/// <param name="maxValue">The value of axis type, corresponding to maximal viewport value.</param>
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;
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// AxisControl for DateTime axes.
/// </summary>
public class DateTimeAxisControl : AxisControl<DateTime>
{
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeAxisControl"/> class.
/// </summary>
public DateTimeAxisControl()
{
LabelProvider = new DateTimeLabelProvider();
TicksProvider = new DateTimeTicksProvider();
MajorLabelProvider = new MajorDateTimeLabelProvider();
ConvertToDouble = dt => dt.Ticks;
Range = new Range<DateTime>(DateTime.Now, DateTime.Now.AddYears(1));
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a label provider for <see cref="System.DateTime"/> ticks.
/// </summary>
public class DateTimeLabelProvider : DateTimeLabelProviderBase
{
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeLabelProvider"/> class.
/// </summary>
public DateTimeLabelProvider() { }
public override UIElement[] CreateLabels(ITicksInfo<DateTime> ticksInfo)
{
object info = ticksInfo.Info;
var ticks = ticksInfo.Ticks;
if (info is DifferenceIn)
{
DifferenceIn diff = (DifferenceIn)info;
DateFormat = GetDateFormat(diff);
}
LabelTickInfo<DateTime> tickInfo = new LabelTickInfo<DateTime> { 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;
}
}
}

View File

@@ -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<DateTime>
{
private string dateFormat;
protected string DateFormat
{
get { return dateFormat; }
set { dateFormat = value; }
}
protected override string GetStringCore(LabelTickInfo<DateTime> 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a ticks provider for ticks of <see cref="T:System.DateTime"/> type.
/// </summary>
public class DateTimeTicksProvider : TimeTicksProviderBase<DateTime>
{
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeTicksProvider"/> class.
/// </summary>
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<DateTime>
{
public MinorDateTimeProvider(ITicksProvider<DateTime> owner) : base(owner) { }
protected override bool IsInside(DateTime value, Range<DateTime> 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);
}
}
}

View File

@@ -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<DateTime>
{
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<DateTime> Members
public abstract ITicksInfo<DateTime> GetTicks(Range<DateTime> range, int ticksCount);
public abstract int DecreaseTickCount(int ticksCount);
public abstract int IncreaseTickCount(int ticksCount);
public abstract ITicksProvider<DateTime> MinorProvider { get; }
public abstract ITicksProvider<DateTime> MajorProvider { get; }
#endregion
}
}

View File

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

View File

@@ -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
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// Represents an axis with ticks of <see cref="System.DateTime"/> type, which can be placed only from bottom or top of <see cref="Plotter"/>.
/// By default is placed from bottom.
/// </summary>
public class HorizontalDateTimeAxis : DateTimeAxis
{
/// <summary>
/// Initializes a new instance of the <see cref="HorizontalDateTimeAxis"/> class.
/// </summary>
public HorizontalDateTimeAxis()
{
Placement = AxisPlacement.Bottom;
}
protected override void ValidatePlacement(AxisPlacement newPlacement)
{
if (newPlacement == AxisPlacement.Left || newPlacement == AxisPlacement.Right)
throw new ArgumentException(Strings.Exceptions.HorizontalAxisCannotBeVertical);
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a label provider for major ticks of <see cref="System.DateTime"/> type.
/// </summary>
public class MajorDateTimeLabelProvider : DateTimeLabelProviderBase
{
/// <summary>
/// Initializes a new instance of the <see cref="MajorDateTimeLabelProvider"/> class.
/// </summary>
public MajorDateTimeLabelProvider() { }
public override UIElement[] CreateLabels(ITicksInfo<DateTime> 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<DateTime> tickInfo = new LabelTickInfo<DateTime>();
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;
}
}
}

View File

@@ -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<T> : ITicksProvider<T>
{
public event EventHandler Changed;
protected void RaiseChanged()
{
if (Changed != null)
{
Changed(this, EventArgs.Empty);
}
}
private readonly ITicksProvider<T> provider;
public MinorTimeProviderBase(ITicksProvider<T> provider)
{
this.provider = provider;
}
private T[] majorTicks = new T[] { };
internal void SetTicks(T[] ticks)
{
this.majorTicks = ticks;
}
private double ticksSize = 0.5;
public ITicksInfo<T> GetTicks(Range<T> range, int ticksCount)
{
if (majorTicks.Length == 0)
return new TicksInfo<T>();
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<T>
{
Ticks = ticks,
TickSizes = ArrayExtensions.CreateArray(ticks.Length, ticksSize)
};
return res;
}
private ITicksInfo<T> Clip(ITicksInfo<T> ticks, Range<T> range)
{
var newTicks = new List<T>(ticks.Ticks.Length);
var newSizes = new List<double>(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<T>
{
Ticks = newTicks.ToArray(),
TickSizes = newSizes.ToArray(),
Info = ticks.Info
};
}
protected abstract bool IsInside(T value, Range<T> 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<T> MinorProvider
{
get { return null; }
}
public ITicksProvider<T> MajorProvider
{
get { return null; }
}
}
}

View File

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

View File

@@ -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<TimeSpan, DifferenceIn?> function;
public DelegateDateTimeStrategy(Func<TimeSpan, DifferenceIn?> 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;
}
}
}

View File

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

View File

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

View File

@@ -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<T> : ITicksProvider<T>
{
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<T> GetTicks(Range<T> 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<T> { 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<T> ticks = new List<T>();
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<T> res = new TicksInfo<T> { 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<T> Trim(List<T> ticks, Range<T> range);
public ITicksProvider<T> MinorProvider
{
get { throw new NotSupportedException(); }
}
public ITicksProvider<T> MajorProvider
{
get { throw new NotSupportedException(); }
}
}
internal abstract class DatePeriodTicksProvider : TimePeriodTicksProvider<DateTime>
{
protected sealed override bool Continue(DateTime current, DateTime end)
{
return current < end;
}
protected sealed override List<DateTime> Trim(List<DateTime> ticks, Range<DateTime> 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<DateTime> res = new List<DateTime>(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;
}
}
}

View File

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

View File

@@ -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<DifferenceIn, ITicksProvider<DateTime>> providers =
new Dictionary<DifferenceIn, ITicksProvider<DateTime>>();
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;
/// <summary>
/// Gets the ticks.
/// </summary>
/// <param name="range">The range.</param>
/// <param name="ticksCount">The ticks count.</param>
/// <returns></returns>
public override ITicksInfo<DateTime> GetTicks(Range<DateTime> range, int ticksCount)
{
Verify.Is(ticksCount > 0);
DateTime start = range.Min;
DateTime end = range.Max;
TimeSpan length = end - start;
diff = GetDifference(length);
TicksInfo<DateTime> res = new TicksInfo<DateTime> { Info = diff };
if (providers.ContainsKey(diff))
{
ITicksInfo<DateTime> 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<DateTime>)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<DateTime> resultTicks = new List<DateTime>();
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;
}
/// <summary>
/// Tries the get lower diff.
/// </summary>
/// <param name="diff">The diff.</param>
/// <param name="lowerDiff">The lower diff.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Decreases the tick count.
/// </summary>
/// <param name="tickCount">The tick count.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Increases the tick count.
/// </summary>
/// <param name="tickCount">The tick count.</param>
/// <returns></returns>
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<DateTime>
{
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<DateTime> GetTicks(Range<DateTime> 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<DateTime> { 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<DateTime> ticks = new List<DateTime>();
DateTime finishTick = AddStep(range.Max, step);
while (tick < finishTick)
{
ticks.Add(tick);
tick = AddStep(tick, step);
if (isMinDateTime)
{
isMinDateTime = false;
step++;
}
}
TicksInfo<DateTime> res = new TicksInfo<DateTime> { 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<DateTime> Members
public MinorTickInfo<DateTime>[] CreateTicks(Range<DateTime> range)
{
int tickCount = tickCounts[1];
ITicksInfo<DateTime> ticks = GetTicks(range, tickCount);
MinorTickInfo<DateTime>[] res = ticks.Ticks.
Select(dt => new MinorTickInfo<DateTime>(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);
}
}
}

View File

@@ -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<DateTime>
{
public abstract ITicksInfo<DateTime> GetTicks(Range<DateTime> 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;
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// Contains default axis value conversions.
/// </summary>
public static class DefaultAxisConversions
{
#region double
private static readonly Func<double, double> doubleToDouble = d => d;
public static Func<double, double> DoubleToDouble
{
get { return DefaultAxisConversions.doubleToDouble; }
}
private static readonly Func<double, double> doubleFromDouble = d => d;
public static Func<double, double> 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<double, System.DateTime> 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<double, DateTime> DateTimeFromDouble
{
get { return dateTimeFromDouble; }
}
private static readonly Func<DateTime, double> dateTimeToDouble = dt => dt.Ticks / 10000000000.0;
public static Func<DateTime, double> 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<double, TimeSpan> 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<double, TimeSpan> TimeSpanFromDouble
{
get { return DefaultAxisConversions.timeSpanFromDouble; }
}
private static readonly Func<TimeSpan, double> timeSpanToDouble = timeSpan =>
{
return timeSpan.Ticks / 10000000000.0;
};
public static Func<TimeSpan, double> TimeSpanToDouble
{
get { return DefaultAxisConversions.timeSpanToDouble; }
}
#endregion
#region integer
private readonly static Func<double, int> intFromDouble = d => (int)d;
public static Func<double, int> IntFromDouble
{
get { return DefaultAxisConversions.intFromDouble; }
}
private readonly static Func<int, double> intToDouble = i => (double)i;
public static Func<int, double> IntToDouble
{
get { return DefaultAxisConversions.intToDouble; }
}
#endregion
}
}

View File

@@ -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<double>
{
public double[] GetTicks(Range<double> 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<double> res = new List<double>();
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<int> TickCount =
new ReadOnlyCollection<int>(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;
}
}
}

View File

@@ -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<T> GetTicks<T>(this ITicksProvider<T> provider, Range<T> range)
{
return provider.GetTicks(range, DefaultTicksCount);
}
}
}

142
Charts/Axes/GeneralAxis.cs Normal file
View File

@@ -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
{
/// <summary>
/// Represents a base class for all DynamicDataDisplay's axes.
/// Has several axis-specific and all WPF-specific properties.
/// </summary>
public abstract class GeneralAxis : ContentControl, IPlotterElement
{
/// <summary>
/// Initializes a new instance of the <see cref="GeneralAxis"/> class.
/// </summary>
protected GeneralAxis() { }
#region Placement property
private AxisPlacement placement = AxisPlacement.Bottom;
/// <summary>
/// Gets or sets the placement of axis - place in ChartPlotter where it should be placed.
/// </summary>
/// <value>The placement.</value>
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;
}
/// <summary>
/// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc.
/// If proposed placement is wrong, throws an ArgumentException.
/// </summary>
/// <param name="newPlacement">The new placement.</param>
protected virtual void ValidatePlacement(AxisPlacement newPlacement) { }
#endregion
protected void RaiseTicksChanged()
{
TicksChanged.Raise(this);
}
public abstract void ForceUpdate();
/// <summary>
/// Occurs when ticks changes.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public event EventHandler TicksChanged;
/// <summary>
/// Gets the screen coordinates of axis ticks.
/// </summary>
/// <value>The screen ticks.</value>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract double[] ScreenTicks { get; }
/// <summary>
/// Gets the screen coordinates of minor ticks.
/// </summary>
/// <value>The minor screen ticks.</value>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract MinorTickInfo<double>[] 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
}
}

View File

@@ -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
{
/// <summary>
/// Represents default implementation of label provider for specified type.
/// </summary>
/// <typeparam name="T">Axis values type.</typeparam>
public class GenericLabelProvider<T> : LabelProviderBase<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="GenericLabelProvider&lt;T&gt;"/> class.
/// </summary>
public GenericLabelProvider() { }
#region ILabelProvider<T> Members
/// <summary>
/// Creates the labels by given ticks info.
/// </summary>
/// <param name="ticksInfo">The ticks info.</param>
/// <returns>
/// Array of <see cref="UIElement"/>s, which are axis labels for specified axis ticks.
/// </returns>
public override UIElement[] CreateLabels(ITicksInfo<T> ticksInfo)
{
var ticks = ticksInfo.Ticks;
var info = ticksInfo.Info;
LabelTickInfo<T> tickInfo = new LabelTickInfo<T>();
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
}
}

View File

@@ -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<TItem, TAxis> : LabelProviderBase<TAxis>
{
private readonly IList<TItem> collection;
private readonly Func<TItem, string> displayMemberMapping;
public GenericLocationalLabelProvider(IList<TItem> collection, Func<TItem, string> 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<TAxis> ticksInfo)
{
var ticks = ticksInfo.Ticks;
if (ticks.Length == 0)
return EmptyLabelsArray;
startIndex = (int)ticksInfo.Info;
UIElement[] result = new UIElement[ticks.Length];
LabelTickInfo<TAxis> labelInfo = new LabelTickInfo<TAxis> { 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<TAxis> tickInfo)
{
return displayMemberMapping(collection[tickInfo.Index + startIndex]);
}
}
}

View File

@@ -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<TCollection, TAxis> : ITicksProvider<TAxis> where TAxis : IComparable<TAxis>
{
private IList<TCollection> collection;
public IList<TCollection> Collection
{
get { return collection; }
set
{
if (value == null)
throw new ArgumentNullException("value");
Changed.Raise(this);
collection = value;
}
}
private Func<TCollection, TAxis> axisMapping;
public Func<TCollection, TAxis> AxisMapping
{
get { return axisMapping; }
set
{
if (value == null)
throw new ArgumentNullException("value");
Changed.Raise(this);
axisMapping = value;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="GenericLocationalTicksProvider&lt;T&gt;"/> class.
/// </summary>
public GenericLocationalTicksProvider() { }
/// <summary>
/// Initializes a new instance of the <see cref="GenericLocationalTicksProvider&lt;T&gt;"/> class.
/// </summary>
/// <param name="collection">The collection of axis ticks and labels.</param>
public GenericLocationalTicksProvider(IList<TCollection> collection)
{
Collection = collection;
}
public GenericLocationalTicksProvider(IList<TCollection> collection, Func<TCollection, TAxis> coordinateMapping)
{
Collection = collection;
AxisMapping = coordinateMapping;
}
#region ITicksProvider<T> Members
SearchResult1d minResult = SearchResult1d.Empty;
SearchResult1d maxResult = SearchResult1d.Empty;
GenericSearcher1d<TCollection, TAxis> searcher;
/// <summary>
/// Generates ticks for given range and preferred ticks count.
/// </summary>
/// <param name="range">The range.</param>
/// <param name="ticksCount">The ticks count.</param>
/// <returns></returns>
public ITicksInfo<TAxis> GetTicks(Range<TAxis> 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<TAxis> result = new TicksInfo<TAxis>
{
Info = startIndex,
TickSizes = ArrayExtensions.CreateArray(count, 1.0),
Ticks = ticks
};
return result;
}
else
{
return TicksInfo<TAxis>.Empty;
}
}
private void EnsureSearcher()
{
if (searcher == null)
{
if (collection == null || axisMapping == null)
throw new InvalidOperationException(Strings.Exceptions.GenericLocationalProviderInvalidState);
searcher = new GenericSearcher1d<TCollection, TAxis>(collection, axisMapping);
}
}
public int DecreaseTickCount(int ticksCount)
{
return collection.Count;
}
public int IncreaseTickCount(int ticksCount)
{
return collection.Count;
}
public ITicksProvider<TAxis> MinorProvider
{
get { return null; }
}
public ITicksProvider<TAxis> MajorProvider
{
get { return null; }
}
public event EventHandler Changed;
#endregion
}
}

View File

@@ -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
{
/// <summary>
/// Contains information about one minor tick - its value (relative size) and its tick.
/// </summary>
/// <typeparam name="T"></typeparam>
[DebuggerDisplay("{Value} @ {Tick}")]
public struct MinorTickInfo<T>
{
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);
}
}
/// <summary>
/// Contains data for all generated ticks.
/// Used by TicksLabelProvider.
/// </summary>
/// <typeparam name="T">Type of axis tick.</typeparam>
public interface ITicksInfo<T>
{
/// <summary>
/// Gets the array of axis ticks.
/// </summary>
/// <value>The ticks.</value>
T[] Ticks { get; }
/// <summary>
/// Gets the tick sizes.
/// </summary>
/// <value>The tick sizes.</value>
double[] TickSizes { get; }
/// <summary>
/// Gets the additional information, added to ticks info and specifying range's features.
/// </summary>
/// <value>The info.</value>
object Info { get; }
}
internal class TicksInfo<T> : ITicksInfo<T>
{
private T[] ticks = { };
/// <summary>
/// Gets the array of axis ticks.
/// </summary>
/// <value>The ticks.</value>
public T[] Ticks
{
get { return ticks; }
internal set { ticks = value; }
}
private double[] tickSizes = { };
/// <summary>
/// Gets the tick sizes.
/// </summary>
/// <value>The tick sizes.</value>
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;
/// <summary>
/// Gets the additional information, added to ticks info and specifying range's features.
/// </summary>
/// <value>The info.</value>
public object Info
{
get { return info; }
internal set { info = value; }
}
private static readonly TicksInfo<T> empty = new TicksInfo<T> { info = null, ticks = new T[0], tickSizes = new double[0] };
internal static TicksInfo<T> Empty
{
get { return empty; }
}
}
/// <summary>
/// Base interface for ticks generator.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ITicksProvider<T>
{
/// <summary>
/// Generates ticks for given range and preferred ticks count.
/// </summary>
/// <param name="range">The range.</param>
/// <param name="ticksCount">The ticks count.</param>
/// <returns></returns>
ITicksInfo<T> GetTicks(Range<T> range, int ticksCount);
/// <summary>
/// Decreases the tick count.
/// Returned value should be later passed as ticksCount parameter to GetTicks method.
/// </summary>
/// <param name="ticksCount">The ticks count.</param>
/// <returns>Decreased ticks count.</returns>
int DecreaseTickCount(int ticksCount);
/// <summary>
/// Increases the tick count.
/// Returned value should be later passed as ticksCount parameter to GetTicks method.
/// </summary>
/// <param name="ticksCount">The ticks count.</param>
/// <returns>Increased ticks count.</returns>
int IncreaseTickCount(int ticksCount);
/// <summary>
/// Gets the minor ticks provider, used to generate ticks between each two adjacent ticks.
/// </summary>
/// <value>The minor provider. If there is no minor provider available, returns null.</value>
ITicksProvider<T> MinorProvider { get; }
/// <summary>
/// Gets the major provider, used to generate major ticks - for example, years for common ticks as months.
/// </summary>
/// <value>The major provider. If there is no major provider available, returns null.</value>
ITicksProvider<T> MajorProvider { get; }
/// <summary>
/// Occurs when properties of ticks provider changeds.
/// Notifies axis to rebuild its view.
/// </summary>
event EventHandler Changed;
}
}

40
Charts/Axes/ITypedAxis.cs Normal file
View File

@@ -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
{
/// <summary>
/// Describes axis as having ticks type.
/// Provides access to some typed properties.
/// </summary>
/// <typeparam name="T">Axis tick's type.</typeparam>
public interface ITypedAxis<T>
{
/// <summary>
/// Gets the ticks provider.
/// </summary>
/// <value>The ticks provider.</value>
ITicksProvider<T> TicksProvider { get; }
/// <summary>
/// Gets the label provider.
/// </summary>
/// <value>The label provider.</value>
LabelProviderBase<T> LabelProvider { get; }
/// <summary>
/// Gets or sets the convertion of tick from double.
/// Should not be null.
/// </summary>
/// <value>The convert from double.</value>
Func<double, T> ConvertFromDouble { get; set; }
/// <summary>
/// Gets or sets the convertion of tick to double.
/// Should not be null.
/// </summary>
/// <value>The convert to double.</value>
Func<T, double> ConvertToDouble { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
public interface IValueConversion<T>
{
Func<T, double> ConvertToDouble { get; set; }
Func<double, T> ConvertFromDouble { get; set; }
}
}

View File

@@ -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<T> : LabelProviderBase<int>
{
private IList<T> collection;
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public IList<T> 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
/// <summary>
/// Initializes a new instance of the <see cref="CollectionLabelProvider&lt;T&gt;"/> class with empty labels collection.
/// </summary>
public CollectionLabelProvider() { }
public CollectionLabelProvider(IList<T> collection)
: this()
{
Collection = collection;
}
public override UIElement[] CreateLabels(ITicksInfo<int> ticksInfo)
{
var ticks = ticksInfo.Ticks;
UIElement[] res = new UIElement[ticks.Length];
var tickInfo = new LabelTickInfo<int> { 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;
}
}
}

View File

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

View File

@@ -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<int>
{
public IntegerAxis()
: base(new IntegerAxisControl(),
d => (int)d,
i => (double)i)
{
}
}
}

View File

@@ -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<int>
{
public IntegerAxisControl()
{
LabelProvider = new GenericLabelProvider<int>();
TicksProvider = new IntegerTicksProvider();
ConvertToDouble = i => (double)i;
Range = new Range<int>(0, 1);
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a ticks provider for intefer values.
/// </summary>
public class IntegerTicksProvider : ITicksProvider<int>
{
/// <summary>
/// Initializes a new instance of the <see cref="IntegerTicksProvider"/> class.
/// </summary>
public IntegerTicksProvider() { }
private int minStep = 0;
/// <summary>
/// Gets or sets the minimal step between ticks.
/// </summary>
/// <value>The min step.</value>
public int MinStep
{
get { return minStep; }
set
{
Verify.IsTrue(value >= 0, "value");
if (minStep != value)
{
minStep = value;
RaiseChangedEvent();
}
}
}
private int maxStep = Int32.MaxValue;
/// <summary>
/// Gets or sets the maximal step between ticks.
/// </summary>
/// <value>The max step.</value>
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<int> Members
/// <summary>
/// Generates ticks for given range and preferred ticks count.
/// </summary>
/// <param name="range">The range.</param>
/// <param name="ticksCount">The ticks count.</param>
/// <returns></returns>
public ITicksInfo<int> GetTicks(Range<int> 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<int> res = new TicksInfo<int> { 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<int> res = new List<int>();
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 };
/// <summary>
/// Decreases the tick count.
/// Returned value should be later passed as ticksCount parameter to GetTicks method.
/// </summary>
/// <param name="ticksCount">The ticks count.</param>
/// <returns>Decreased ticks count.</returns>
public int DecreaseTickCount(int ticksCount)
{
return tickCounts.FirstOrDefault(tick => tick < ticksCount);
}
/// <summary>
/// Increases the tick count.
/// Returned value should be later passed as ticksCount parameter to GetTicks method.
/// </summary>
/// <param name="ticksCount">The ticks count.</param>
/// <returns>Increased ticks count.</returns>
public int IncreaseTickCount(int ticksCount)
{
int newTickCount = tickCounts.Reverse().FirstOrDefault(tick => tick > ticksCount);
if (newTickCount == 0)
newTickCount = tickCounts[0];
return newTickCount;
}
/// <summary>
/// Gets the minor ticks provider, used to generate ticks between each two adjacent ticks.
/// </summary>
/// <value>The minor provider.</value>
public ITicksProvider<int> MinorProvider
{
get { return null; }
}
/// <summary>
/// Gets the major provider, used to generate major ticks - for example, years for common ticks as months.
/// </summary>
/// <value>The major provider.</value>
public ITicksProvider<int> MajorProvider
{
get { return null; }
}
protected void RaiseChangedEvent()
{
Changed.Raise(this);
}
public event EventHandler Changed;
#endregion
}
}

View File

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

View File

@@ -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<T> : LabelProviderBase<T>
{
public override UIElement[] CreateLabels(ITicksInfo<T> ticksInfo)
{
var ticks = ticksInfo.Ticks;
UIElement[] res = new UIElement[ticks.Length];
LabelTickInfo<T> labelInfo = new LabelTickInfo<T> { 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Contains data for custom generation of tick's label.
/// </summary>
/// <typeparam name="T">Type of ticks</typeparam>
public sealed class LabelTickInfo<T>
{
internal LabelTickInfo() { }
/// <summary>
/// Gets or sets the tick.
/// </summary>
/// <value>The tick.</value>
public T Tick { get; internal set; }
/// <summary>
/// Gets or sets additional info about ticks range.
/// </summary>
/// <value>The info.</value>
public object Info { get; internal set; }
/// <summary>
/// Gets or sets the index of tick in ticks array.
/// </summary>
/// <value>The index.</value>
public int Index { get; internal set; }
}
/// <summary>
/// Base class for all label providers.
/// Contains a number of properties that can be used to adjust generated labels.
/// </summary>
/// <typeparam name="T">Type of ticks, which labels are generated for</typeparam>
/// <remarks>
/// 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.
/// </remarks>
public abstract class LabelProviderBase<T>
{
#region Private
private string labelStringFormat = null;
private Func<LabelTickInfo<T>, string> customFormatter = null;
private Action<LabelTickInfo<T>, UIElement> customView = null;
#endregion
private static readonly UIElement[] emptyLabelsArray = new UIElement[0];
protected static UIElement[] EmptyLabelsArray
{
get { return emptyLabelsArray; }
}
/// <summary>
/// Creates labels by given ticks info.
/// Is not intended to be called from your code.
/// </summary>
/// <param name="ticksInfo">The ticks info.</param>
/// <returns>Array of <see cref="UIElement"/>s, which are axis labels for specified axis ticks.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract UIElement[] CreateLabels(ITicksInfo<T> ticksInfo);
/// <summary>
/// Gets or sets the label string format.
/// </summary>
/// <value>The label string format.</value>
public string LabelStringFormat
{
get { return labelStringFormat; }
set
{
if (labelStringFormat != value)
{
labelStringFormat = value;
RaiseChanged();
}
}
}
/// <summary>
/// Gets or sets the custom formatter - delegate that can be called to create custom string representation of tick.
/// </summary>
/// <value>The custom formatter.</value>
public Func<LabelTickInfo<T>, string> CustomFormatter
{
get { return customFormatter; }
set
{
if (customFormatter != value)
{
customFormatter = value;
RaiseChanged();
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <value>The custom view.</value>
public Action<LabelTickInfo<T>, UIElement> CustomView
{
get { return customView; }
set
{
if (customView != value)
{
customView = value;
RaiseChanged();
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="formatter">The formatter.</param>
public void SetCustomFormatter(Func<LabelTickInfo<T>, string> formatter)
{
CustomFormatter = formatter;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="view">The view.</param>
public void SetCustomView(Action<LabelTickInfo<T>, UIElement> view)
{
CustomView = view;
}
protected virtual string GetString(LabelTickInfo<T> 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<T> tickInfo)
{
return tickInfo.Tick.ToString();
}
protected void ApplyCustomView(LabelTickInfo<T> info, UIElement label)
{
if (CustomView != null)
{
CustomView(info, label);
}
}
/// <summary>
/// Occurs when label provider is changed.
/// Notifies axis to update its view.
/// </summary>
public event EventHandler Changed;
protected void RaiseChanged()
{
Changed.Raise(this);
}
private readonly ResourcePool<UIElement> pool = new ResourcePool<UIElement>();
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();
}
}
}

View File

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

View File

@@ -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<double>
{
private double customBase = 2;
/// <summary>
/// Gets or sets the custom base.
/// </summary>
/// <value>The custom base.</value>
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;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomBaseNumericLabelProvider"/> class.
/// </summary>
public CustomBaseNumericLabelProvider() { }
/// <summary>
/// Initializes a new instance of the <see cref="CustomBaseNumericLabelProvider"/> class.
/// </summary>
public CustomBaseNumericLabelProvider(double customBase)
: this()
{
CustomBase = customBase;
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomBaseNumericLabelProvider"/> class.
/// </summary>
/// <param name="customBase">The custom base.</param>
/// <param name="customBaseString">The custom base string.</param>
public CustomBaseNumericLabelProvider(double customBase, string customBaseString)
: this(customBase)
{
CustomBaseString = customBaseString;
}
private string customBaseString = null;
/// <summary>
/// Gets or sets the custom base string.
/// </summary>
/// <value>The custom base string.</value>
public string CustomBaseString
{
get { return customBaseString; }
set
{
if (customBaseString != value)
{
customBaseString = value;
RaiseChanged();
}
}
}
protected override string GetStringCore(LabelTickInfo<double> 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;
}
}
}

View File

@@ -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<double>
{
private double customBase = 2;
/// <summary>
/// Gets or sets the custom base.
/// </summary>
/// <value>The custom base.</value>
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;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomBaseNumericTicksProvider"/> class.
/// </summary>
public CustomBaseNumericTicksProvider() : this(2.0) { }
/// <summary>
/// Initializes a new instance of the <see cref="CustomBaseNumericTicksProvider"/> class.
/// </summary>
/// <param name="customBase">The custom base, e.g. Math.PI</param>
public CustomBaseNumericTicksProvider(double customBase) : this(customBase, new NumericTicksProvider()) { }
private CustomBaseNumericTicksProvider(double customBase, ITicksProvider<double> 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<double> ticksProvider = null;
public ITicksProvider<double> 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<double> TransformRange(Range<double> range)
{
double min = range.Min / customBase;
double max = range.Max / customBase;
return new Range<double>(min, max);
}
#region ITicksProvider<double> Members
private double[] tickMarks;
public ITicksInfo<double> GetTicks(Range<double> range, int ticksCount)
{
var ticks = ticksProvider.GetTicks(TransformRange(range), ticksCount);
TransformTicks(ticks);
tickMarks = ticks.Ticks;
return ticks;
}
private void TransformTicks(ITicksInfo<double> 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<double> minorTicksProvider;
public ITicksProvider<double> MinorProvider
{
get { return minorTicksProvider; }
}
/// <summary>
/// Gets the major provider, used to generate major ticks - for example, years for common ticks as months.
/// </summary>
/// <value>The major provider.</value>
public ITicksProvider<double> MajorProvider
{
get { return null; }
}
public event EventHandler Changed;
#endregion
private sealed class MinorProviderWrapper : ITicksProvider<double>
{
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<double> MinorTicksProvider
{
get { return owner.ticksProvider.MinorProvider; }
}
#region ITicksProvider<double> Members
public ITicksInfo<double> GetTicks(Range<double> 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<double> MinorProvider
{
get { return MinorTicksProvider.MinorProvider; }
}
public ITicksProvider<double> MajorProvider
{
get { return owner; }
}
public event EventHandler Changed;
#endregion
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents an axis label provider for double ticks, generating labels with numbers in exponential form when it is appropriate.
/// </summary>
public sealed class ExponentialLabelProvider : NumericLabelProviderBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ExponentialLabelProvider"/> class.
/// </summary>
public ExponentialLabelProvider() { }
/// <summary>
/// Creates labels by given ticks info.
/// Is not intended to be called from your code.
/// </summary>
/// <param name="ticksInfo">The ticks info.</param>
/// <returns>
/// Array of <see cref="UIElement"/>s, which are axis labels for specified axis ticks.
/// </returns>
public override UIElement[] CreateLabels(ITicksInfo<double> ticksInfo)
{
var ticks = ticksInfo.Ticks;
Init(ticks);
UIElement[] res = new UIElement[ticks.Length];
LabelTickInfo<double> tickInfo = new LabelTickInfo<double> { 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;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// Represents a horizontal axis with values of <see cref="Double"/> type.
/// Can be placed only from bottom or top side of plotter.
/// By default is placed from the bottom side.
/// </summary>
public class HorizontalAxis : NumericAxis
{
/// <summary>
/// Initializes a new instance of the <see cref="HorizontalAxis"/> class, placed on bottom of <see cref="ChartPlotter"/>.
/// </summary>
public HorizontalAxis()
{
Placement = AxisPlacement.Bottom;
}
/// <summary>
/// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc.
/// If proposed placement is wrong, throws an ArgumentException.
/// </summary>
/// <param name="newPlacement">The new placement.</param>
protected override void ValidatePlacement(AxisPlacement newPlacement)
{
if (newPlacement == AxisPlacement.Left || newPlacement == AxisPlacement.Right)
throw new ArgumentException(Strings.Exceptions.HorizontalAxisCannotBeVertical);
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a ticks provider for logarithmically transfomed axis - returns ticks which are a power of specified logarithm base.
/// </summary>
public class LogarithmNumericTicksProvider : ITicksProvider<double>
{
/// <summary>
/// Initializes a new instance of the <see cref="LogarithmNumericTicksProvider"/> class.
/// </summary>
public LogarithmNumericTicksProvider()
{
minorProvider = new MinorNumericTicksProvider(this);
minorProvider.Changed += ticksProvider_Changed;
}
/// <summary>
/// Initializes a new instance of the <see cref="LogarithmNumericTicksProvider"/> class.
/// </summary>
/// <param name="logarithmBase">The logarithm base.</param>
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<double> Members
private double[] ticks;
public ITicksInfo<double> GetTicks(Range<double> 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<double> result = new TicksInfo<double> { Ticks = ticks, TickSizes = ArrayExtensions.CreateArray(ticks.Length, 1.0), Info = log };
return result;
}
private double[] CreateTicks(Range<double> 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<double> MinorProvider
{
get
{
minorProvider.SetRanges(ArrayExtensions.GetPairs(ticks));
return minorProvider;
}
}
public ITicksProvider<double> MajorProvider
{
get { return null; }
}
public event EventHandler Changed;
#endregion
}
}

View File

@@ -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<double>
{
private readonly ITicksProvider<double> parent;
private Range<double>[] ranges;
internal void SetRanges(IEnumerable<Range<double>> 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<double> 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<double> Members
public event EventHandler Changed;
public ITicksInfo<double> GetTicks(Range<double> range, int ticksCount)
{
if (Coeffs.Length == 0)
return new TicksInfo<double>();
var minorTicks = ranges.Select(r => CreateTicks(r)).SelectMany(m => m);
var res = new TicksInfo<double>();
res.TickSizes = minorTicks.Select(m => m.Value).ToArray();
res.Ticks = minorTicks.Select(m => m.Tick).ToArray();
return res;
}
public MinorTickInfo<double>[] CreateTicks(Range<double> range)
{
double step = (range.Max - range.Min) / (Coeffs.Length + 1);
MinorTickInfo<double>[] res = new MinorTickInfo<double>[Coeffs.Length];
for (int i = 0; i < Coeffs.Length; i++)
{
res[i] = new MinorTickInfo<double>(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<double> MinorProvider
{
get { return null; }
}
public ITicksProvider<double> MajorProvider
{
get { return parent; }
}
#endregion
}
}

View File

@@ -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
{
/// <summary>
/// Represents a numeric axis with values of <see cref="System.Double"/> type.
/// </summary>
public class NumericAxis : AxisBase<double>
{
/// <summary>
/// Initializes a new instance of the <see cref="NumericAxis"/> class.
/// </summary>
public NumericAxis()
: base(new NumericAxisControl(),
d => d,
d => d)
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="min">The minimal viewport value.</param>
/// <param name="minValue">The value of axis type, corresponding to minimal viewport value.</param>
/// <param name="max">The maximal viewport value.</param>
/// <param name="maxValue">The value of axis type, corresponding to maximal viewport value.</param>
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;
}
}
}

View File

@@ -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<double>
{
public NumericAxisControl()
{
LabelProvider = new ExponentialLabelProvider();
TicksProvider = new NumericTicksProvider();
ConvertToDouble = d => d;
Range = new Range<double>(0, 10);
}
}
}

View File

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

View File

@@ -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<double>
{
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<double> 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a ticks provider for <see cref="System.Double"/> values.
/// </summary>
public sealed class NumericTicksProvider : ITicksProvider<double>
{
/// <summary>
/// Initializes a new instance of the <see cref="NumericTicksProvider"/> class.
/// </summary>
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;
/// <summary>
/// Gets or sets the minimal step between ticks.
/// </summary>
/// <value>The min step.</value>
public double MinStep
{
get { return minStep; }
set
{
Verify.IsTrue(value >= 0.0, "value");
if (minStep != value)
{
minStep = value;
RaiseChangedEvent();
}
}
}
private double[] ticks;
public ITicksInfo<double> GetTicks(Range<double> 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<double> res = new TicksInfo<double> { 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<double> res = new List<double>();
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<double> MinorProvider
{
get
{
if (ticks != null)
{
minorProvider.SetRanges(ticks.GetPairs());
}
return minorProvider;
}
}
public ITicksProvider<double> MajorProvider
{
get { return null; }
}
}
}

View File

@@ -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
{
/// <summary>
/// Represents a simple label provider for double ticks, which simply returns result of .ToString() method, called for rounded ticks.
/// </summary>
public class ToStringLabelProvider : NumericLabelProviderBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ToStringLabelProvider"/> class.
/// </summary>
public ToStringLabelProvider() { }
public override UIElement[] CreateLabels(ITicksInfo<double> ticksInfo)
{
var ticks = ticksInfo.Ticks;
Init(ticks);
UIElement[] res = new UIElement[ticks.Length];
LabelTickInfo<double> tickInfo = new LabelTickInfo<double> { 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;
}
}
}

View File

@@ -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<double>
{
}
}

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class VerticalAxis : NumericAxis
{
/// <summary>
/// Initializes a new instance of the <see cref="VerticalAxis"/> class.
/// </summary>
public VerticalAxis()
{
Placement = AxisPlacement.Left;
}
/// <summary>
/// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc.
/// If proposed placement if wrong, throws an ArgumentException.
/// </summary>
/// <param name="newPlacement">The new placement.</param>
protected override void ValidatePlacement(AxisPlacement newPlacement)
{
if (newPlacement == AxisPlacement.Bottom || newPlacement == AxisPlacement.Top)
throw new ArgumentException(Strings.Exceptions.VerticalAxisCannotBeHorizontal);
}
}
}

View File

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

View File

@@ -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<double> 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; }
}
}

237
Charts/Axes/StackCanvas.cs Normal file
View File

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

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// Represents a horizontal axis with values of <see cref="TimeSpan"/> type.
/// </summary>
public class HorizontalTimeSpanAxis : TimeSpanAxis
{
/// <summary>
/// Initializes a new instance of the <see cref="HorizontalTimeSpanAxis"/> class, placed on the bottom of <see cref="ChartPlotter"/>.
/// </summary>
public HorizontalTimeSpanAxis()
{
Placement = AxisPlacement.Bottom;
}
protected override void ValidatePlacement(AxisPlacement newPlacement)
{
if (newPlacement == AxisPlacement.Left || newPlacement == AxisPlacement.Right)
throw new ArgumentException(Strings.Exceptions.HorizontalAxisCannotBeVertical);
}
}
}

View File

@@ -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<TimeSpan>
{
public MinorTimeSpanTicksProvider(ITicksProvider<TimeSpan> owner) : base(owner) { }
protected override bool IsInside(TimeSpan value, Range<TimeSpan> range)
{
return range.Min < value && value < range.Max;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// Represents axis with values of type TimeSpan.
/// </summary>
public class TimeSpanAxis : AxisBase<TimeSpan>
{
/// <summary>
/// Initializes a new instance of the <see cref="TimeSpanAxis"/> class with default values conversion.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="min">The minimal viewport value.</param>
/// <param name="minValue">The value of axis type, corresponding to minimal viewport value.</param>
/// <param name="max">The maximal viewport value.</param>
/// <param name="maxValue">The value of axis type, corresponding to maximal viewport value.</param>
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;
}
}
}

View File

@@ -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<TimeSpan>
{
public TimeSpanAxisControl()
{
LabelProvider = new TimeSpanLabelProvider();
TicksProvider = new TimeSpanTicksProvider();
ConvertToDouble = time => time.Ticks;
Range = new Range<TimeSpan>(new TimeSpan(), new TimeSpan(1, 0, 0));
}
}
}

View File

@@ -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<TimeSpan>
{
public override UIElement[] CreateLabels(ITicksInfo<TimeSpan> ticksInfo)
{
object info = ticksInfo.Info;
var ticks = ticksInfo.Ticks;
LabelTickInfo<TimeSpan> tickInfo = new LabelTickInfo<TimeSpan>();
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;
}
}
}

View File

@@ -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<TimeSpan>
{
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;
}
}
}

View File

@@ -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<TimeSpan>
{
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<TimeSpan> Trim(List<TimeSpan> ticks, Range<TimeSpan> 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<TimeSpan> res = new List<TimeSpan>(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));
}
}
}

View File

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

View File

@@ -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<T> : ITicksProvider<T>
{
public event EventHandler Changed;
protected void RaiseChanged()
{
if (Changed != null)
{
Changed(this, EventArgs.Empty);
}
}
private static readonly Dictionary<DifferenceIn, ITicksProvider<T>> providers =
new Dictionary<DifferenceIn, ITicksProvider<T>>();
protected static Dictionary<DifferenceIn, ITicksProvider<T>> Providers
{
get { return TimeTicksProviderBase<T>.providers; }
}
private static readonly Dictionary<DifferenceIn, ITicksProvider<T>> minorProviders =
new Dictionary<DifferenceIn, ITicksProvider<T>>();
protected static Dictionary<DifferenceIn, ITicksProvider<T>> MinorProviders
{
get { return TimeTicksProviderBase<T>.minorProviders; }
}
protected abstract TimeSpan GetDifference(T start, T end);
#region ITicksProvider<T> Members
private IDateTimeTicksStrategy strategy = new DefaultDateTimeTicksStrategy();
public IDateTimeTicksStrategy Strategy
{
get { return strategy; }
set
{
if (strategy != value)
{
strategy = value;
RaiseChanged();
}
}
}
private ITicksInfo<T> result;
private DifferenceIn diff;
public ITicksInfo<T> GetTicks(Range<T> 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<T> result = new TicksInfo<T> { Info = diff };
if (providers.ContainsKey(diff))
{
ITicksInfo<T> 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;
}
/// <summary>
/// Decreases the tick count.
/// </summary>
/// <param name="tickCount">The tick count.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Increases the tick count.
/// </summary>
/// <param name="ticksCount">The tick count.</param>
/// <returns></returns>
public int IncreaseTickCount(int ticksCount)
{
DebugVerify.Is(ticksCount < 2000);
if (providers.ContainsKey(diff))
return providers[diff].IncreaseTickCount(ticksCount);
return ticksCount * 2;
}
public ITicksProvider<T> MinorProvider
{
get
{
DifferenceIn smallerDiff = DifferenceIn.Smallest;
if (strategy.TryGetLowerDiff(diff, out smallerDiff) && minorProviders.ContainsKey(smallerDiff))
{
var minorProvider = (MinorTimeProviderBase<T>)minorProviders[smallerDiff];
minorProvider.SetTicks(result.Ticks);
return minorProvider;
}
return null;
// todo What to do if this already is the smallest provider?
}
}
public ITicksProvider<T> 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
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts
{
/// <summary>
/// Represents a vertical axis with values of <see cref="TimeSpan"/> type.
/// </summary>
public class VerticalTimeSpanAxis : TimeSpanAxis
{
/// <summary>
/// Initializes a new instance of the <see cref="VerticalTimeSpanAxis"/> class, placed (by default) on the left side of <see cref="ChartPlotter"/>.
/// </summary>
public VerticalTimeSpanAxis()
{
Placement = AxisPlacement.Left;
}
/// <summary>
/// Validates the placement - e.g., vertical axis should not be placed from top or bottom, etc.
/// If proposed placement is wrong, throws an ArgumentException.
/// </summary>
/// <param name="newPlacement">The new placement.</param>
protected override void ValidatePlacement(AxisPlacement newPlacement)
{
if (newPlacement == AxisPlacement.Bottom || newPlacement == AxisPlacement.Top)
throw new ArgumentException(Strings.Exceptions.VerticalAxisCannotBeHorizontal);
}
}
}

View File

@@ -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
}
}

341
Charts/BitmapBasedGraph.cs Normal file
View File

@@ -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;
/// <summary>Latest complete request</summary>
private RenderRequest completedRequest = null;
/// <summary>Currently running request</summary>
private RenderRequest activeRequest = null;
/// <summary>Result of latest complete request</summary>
private BitmapSource completedBitmap = null;
/// <summary>Pending render request</summary>
private RenderRequest pendingRequest = null;
/// <summary>Single apartment thread used for background rendering</summary>
/// <remarks>STA is required for creating WPF components in this thread</remarks>
private Thread renderThread = null;
private AutoResetEvent renderRequested = new AutoResetEvent(false);
private ManualResetEvent shutdownRequested = new ManualResetEvent(false);
/// <summary>True means that current bitmap is invalidated and is to be re-rendered.</summary>
private bool bitmapInvalidated = true;
/// <summary>Shows tooltips.</summary>
private PopupTip popup;
/// <summary>
/// Initializes a new instance of the <see cref="MarkerPointsGraph"/> class.
/// </summary>
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();
}
/// <summary>
/// Adds a render task and invalidates visual.
/// </summary>
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;
/// <summary>Constructs successul rendering result</summary>
/// <param name="request">Source request</param>
/// <param name="result">Rendered bitmap</param>
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);
}
}
}

89
Charts/ContentGraph.cs Normal file
View File

@@ -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
}
}

361
Charts/DataFollowChart.cs Normal file
View File

@@ -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
{
/// <summary>
/// Represents a marker with position.X bound to mouse cursor's position and position.Y is determined by interpolation of <see cref="MarkerPointsGraph"/>'s points.
/// </summary>
[ContentProperty("MarkerTemplate")]
public class DataFollowChart : ViewportHostPanel, INotifyPropertyChanged
{
/// <summary>
/// Initializes a new instance of the <see cref="DataFollowChart"/> class.
/// </summary>
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
};
}
/// <summary>
/// Initializes a new instance of the <see cref="DataFollowChart"/> class, bound to specified <see cref="PointsGraphBase"/>.
/// </summary>
/// <param name="pointSource">The point source.</param>
public DataFollowChart(PointsGraphBase pointSource)
: this()
{
PointSource = pointSource;
}
#region MarkerTemplate property
/// <summary>
/// Gets or sets the template, used to create a marker. This is a dependency property.
/// </summary>
/// <value>The marker template.</value>
public DataTemplate MarkerTemplate
{
get { return (DataTemplate)GetValue(MarkerTemplateProperty); }
set { SetValue(MarkerTemplateProperty, value); }
}
/// <summary>
/// Identifies the <see cref="MarkerTemplate"/> dependency property.
/// </summary>
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
/// <summary>
/// Gets or sets the source of points.
/// Can be null.
/// </summary>
/// <value>The point source.</value>
public PointsGraphBase PointSource
{
get { return (PointsGraphBase)GetValue(PointSourceProperty); }
set { SetValue(PointSourceProperty, value); }
}
/// <summary>
/// Identifies the <see cref="PointSource"/> dependency property.
/// </summary>
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<Point> 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);
}
/// <summary>
/// Identifies the <see cref="MarkerPosition"/> dependency property.
/// </summary>
public static readonly DependencyProperty MarkerPositionProperty = MarkerPositionPropertyKey.DependencyProperty;
/// <summary>
/// Gets the marker position.
/// </summary>
/// <value>The marker position.</value>
public Point MarkerPosition
{
get { return (Point)GetValue(MarkerPositionProperty); }
}
/// <summary>
/// Occurs when marker position changes.
/// </summary>
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
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
/// <summary>
/// Represents a special data context, which encapsulates marker's position and custom data.
/// Used in <see cref="DataFollowChart"/>.
/// </summary>
public class FollowDataContext : INotifyPropertyChanged
{
private Point position;
/// <summary>
/// Gets or sets the position of marker.
/// </summary>
/// <value>The position.</value>
public Point Position
{
get { return position; }
set
{
position = value;
PropertyChanged.Raise(this, "Position");
}
}
private object data;
/// <summary>
/// Gets or sets the additional custom data.
/// </summary>
/// <value>The data.</value>
public object Data
{
get { return data; }
set
{
data = value;
PropertyChanged.Raise(this, "Data");
}
}
#region INotifyPropertyChanged Members
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}

View File

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

61
Charts/DebugMenu.cs Normal file
View File

@@ -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
{
/// <summary>
/// Represents a menu that appears in Debug version of DynamicDataDisplay.
/// </summary>
public class DebugMenu : IPlotterElement
{
/// <summary>
/// Initializes a new instance of the <see cref="DebugMenu"/> class.
/// </summary>
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<MenuItem>().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
}
}

136
Charts/FakePointList.cs Normal file
View File

@@ -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<Point> {
private int first;
private int last;
private int count;
private Point startPoint;
private bool hasPoints;
private double leftBound;
private double rightBound;
private readonly List<Point> points;
internal FakePointList(List<Point> 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<Point> 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<Point> 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<Point> Members
public IEnumerator<Point> 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
}
}

View File

@@ -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
{
/// <summary>
/// Represents a collection of point filters of <see cref="LineGraph"/>.
/// </summary>
public sealed class FilterCollection : D3Collection<IPointsFilter>
{
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<Point> Filter(List<Point> points, Rect screenRect)
{
foreach (var filter in Items)
{
filter.SetScreenRect(screenRect);
points = filter.Filter(points);
}
return points;
}
}
}

View File

@@ -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<Point> Filter(List<Point> points)
{
return points;
}
}
}

View File

@@ -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
{
/// <summary>Visible region in screen coordinates</summary>
private Rect screenRect;
#region IPointFilter Members
public override void SetScreenRect(Rect screenRect)
{
this.screenRect = screenRect;
}
// todo probably use LINQ here.
public override List<Point> Filter(List<Point> points)
{
if (points.Count == 0) return points;
List<Point> resultPoints = points;
List<Point> currentChain = new List<Point>();
if (points.Count > 2 * screenRect.Width)
{
resultPoints = new List<Point>();
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<Point> points)
{
Point minPoint = points[0];
foreach (Point p in points)
{
if (p.X < minPoint.X)
{
minPoint = p;
}
}
return minPoint;
}
private static Point MaxByX(IList<Point> points)
{
Point maxPoint = points[0];
foreach (Point p in points)
{
if (p.X > maxPoint.X)
{
maxPoint = p;
}
}
return maxPoint;
}
private static Point MinByY(IList<Point> points)
{
Point minPoint = points[0];
foreach (Point p in points)
{
if (p.Y < minPoint.Y)
{
minPoint = p;
}
}
return minPoint;
}
private static Point MaxByY(IList<Point> points)
{
Point maxPoint = points[0];
foreach (Point p in points)
{
if (p.Y > maxPoint.Y)
{
maxPoint = p;
}
}
return maxPoint;
}
}
}

View File

@@ -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<Point> Filter(List<Point> points)
{
List<Point> result = new List<Point>();
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;
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Windows;
using System;
namespace Microsoft.Research.DynamicDataDisplay.Filters
{
/// <summary>Provides algorithm for filtering point lists in screen coordinates</summary>
public interface IPointsFilter
{
/// <summary>Performs filtering</summary>
/// <param name="points">List of source points</param>
/// <returns>List of filtered points</returns>
List<Point> Filter(List<Point> points);
/// <summary>Sets visible rectangle in screen coordinates</summary>
/// <param name="rect">Screen rectangle</param>
/// <remarks>Should be invoked before first call to <see cref="Filter"/></remarks>
void SetScreenRect(Rect screenRect);
event EventHandler Changed;
}
}

View File

@@ -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<Point> Filter(List<Point> points)
{
if (points.Count == 0)
return points;
List<Point> res = new List<Point> { 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
}
}

View File

@@ -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<Point> Filter(List<Point> points);
public virtual void SetScreenRect(Rect screenRect) { }
protected void RaiseChanged()
{
Changed.Raise(this);
}
public event EventHandler Changed;
#endregion
}
}

View File

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

View File

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

399
Charts/Isolines/CellInfo.cs Normal file
View File

@@ -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
{
/// <summary>
/// Isoline's grid cell
/// </summary>
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;
/// <summary>Initializes values in four corners of cell</summary>
/// <param name="leftBottom"></param>
/// <param name="rightBottom"></param>
/// <param name="rightTop"></param>
/// <param name="leftTop"></param>
/// <remarks>Some or all values can be NaN. That means that value is not specified (misssing)</remarks>
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<double> 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
/// <summary>
/// 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
/// </summary>
/// <param name="a">Value at corner (see figure)</param>
/// <param name="b">Value at corner (see figure)</param>
/// <param name="c">Value at corner (see figure)</param>
/// <param name="d">Value at corner (see figure)</param>
/// <param name="value">Reference value</param>
/// <returns>Bitmask</returns>
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;
}
}
}

67
Charts/Isolines/Enums.cs Normal file
View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Microsoft.Research.DynamicDataDisplay.Charts.Isolines
{
/// <summary>
/// Edge identifier - indicates which side of cell isoline crosses.
/// </summary>
internal enum Edge
{
// todo check if everything is ok with None.
None = 0,
/// <summary>
/// Isoline crosses left boundary of cell (bit 0)
/// </summary>
Left = 1,
/// <summary>
/// Isoline crosses top boundary of cell (bit 1)
/// </summary>
Top = 2,
/// <summary>
/// Isoline crosses right boundary of cell (bit 2)
/// </summary>
Right = 4,
/// <summary>
/// Isoline crosses bottom boundary of cell (bit 3)
/// </summary>
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;
}
}
}
}

View File

@@ -0,0 +1,19 @@
<isolines:IsolineGraphBase x:Class="Microsoft.Research.DynamicDataDisplay.Charts.Isolines.FastIsolineDisplay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:isolines="clr-namespace:Microsoft.Research.DynamicDataDisplay.Charts.Isolines"
xmlns:local="clr-namespace:Microsoft.Research.DynamicDataDisplay.Charts"
xmlns:common="clr-namespace:Microsoft.Research.DynamicDataDisplay"
IsHitTestVisible="False"
>
<isolines:IsolineGraphBase.Template>
<ControlTemplate TargetType="{x:Type isolines:IsolineGraphBase}">
<Canvas IsHitTestVisible="False">
<local:ViewportHostPanel Name="PART_ViewportPanel">
<isolines:FastIsolineRenderer Name="PART_IsolineRenderer"/>
<isolines:AdditionalLinesRenderer Name="PART_AdditionalLinesRenderer"/>
</local:ViewportHostPanel>
</Canvas>
</ControlTemplate>
</isolines:IsolineGraphBase.Template>
</isolines:IsolineGraphBase>

View File

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

View File

@@ -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<IsolineCollection> additionalLines = new List<IsolineCollection>();
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<double> 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Generates geometric object for isolines of the input 2d scalar field.
/// </summary>
public sealed class IsolineBuilder
{
/// <summary>
/// The density of isolines means the number of levels to draw.
/// </summary>
private int density = 12;
private bool[,] processed;
/// <summary>Number to be treated as missing value. NaN if no missing value is specified</summary>
private double missingValue = Double.NaN;
static IsolineBuilder()
{
SetCellDictionaries();
}
/// <summary>
/// Initializes a new instance of the <see cref="IsolineBuilder"/> class.
/// </summary>
public IsolineBuilder() { }
/// <summary>
/// Initializes a new instance of the <see cref="IsolineBuilder"/> class for specified 2d scalar data source.
/// </summary>
/// <param name="dataSource">The data source with 2d scalar data.</param>
public IsolineBuilder(IDataSource2D<double> dataSource)
{
DataSource = dataSource;
}
public double MissingValue
{
get
{
return missingValue;
}
set
{
missingValue = value;
}
}
#region Private methods
private static Dictionary<int, Dictionary<int, Edge>> dictChooser = new Dictionary<int, Dictionary<int, Edge>>();
private static void SetCellDictionaries()
{
var bottomDict = new Dictionary<int, Edge>();
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<int, Edge>();
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<int, Edge>();
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<int, Edge>();
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)));
}
/// <summary>Finds isoline for specified reference value</summary>
/// <param name="value">Reference value</param>
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);
}
}
}
/// <summary>
/// Builds isoline data for 2d scalar field contained in data source.
/// </summary>
/// <returns>Collection of data describing built isolines.</returns>
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;
}
/// <summary>
/// Builds isoline data for the specified level in 2d scalar field.
/// </summary>
/// <param name="level">The level.</param>
/// <returns></returns>
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<double> minMax;
private IDataSource2D<double> dataSource;
/// <summary>
/// Gets or sets the data source - 2d scalar field.
/// </summary>
/// <value>The data source.</value>
public IDataSource2D<double> 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// LevelLine contains all data for one isoline line - its start point, other points and value in field.
/// </summary>
public sealed class LevelLine
{
/// <summary>
/// Gets or sets the value of line in limits of [0..1].
/// </summary>
/// <value>The value01.</value>
public double Value01 { get; set; }
/// <summary>
/// Gets or sets the real value of line - without scaling to [0..1] segment.
/// </summary>
/// <value>The real value.</value>
public double RealValue { get; set; }
/// <summary>
/// Gets or sets the start point of line.
/// </summary>
/// <value>The start point.</value>
public Point StartPoint { get; set; }
private readonly List<Point> otherPoints = new List<Point>();
/// <summary>
/// Gets other points of line, except first point.
/// </summary>
/// <value>The other points.</value>
public List<Point> OtherPoints
{
get { return otherPoints; }
}
/// <summary>
/// Gets all points of line, including start point.
/// </summary>
/// <value>All points.</value>
public IEnumerable<Point> AllPoints
{
get
{
yield return StartPoint;
for (int i = 0; i < otherPoints.Count; i++)
{
yield return otherPoints[i];
}
}
}
/// <summary>
/// Gets all the segments of lines.
/// </summary>
/// <returns></returns>
public IEnumerable<Range<Point>> GetSegments()
{
if (otherPoints.Count < 1)
yield break;
yield return new Range<Point>(StartPoint, otherPoints[0]);
for (int i = 1; i < otherPoints.Count; i++)
{
yield return new Range<Point>(otherPoints[i - 1], otherPoints[i]);
}
}
}
/// <summary>
/// IsolineTextLabel contains information about one label in isoline - its text, position and rotation.
/// </summary>
public sealed class IsolineTextLabel
{
/// <summary>
/// Gets or sets the rotation of isoline text label.
/// </summary>
/// <value>The rotation.</value>
public double Rotation { get; internal set; }
/// <summary>
/// Gets or sets the text of isoline label.
/// </summary>
/// <value>The text.</value>
public double Value { get; internal set; }
/// <summary>
/// Gets or sets the position of isoline text label.
/// </summary>
/// <value>The position.</value>
public Point Position { get; internal set; }
}
/// <summary>
/// Collection which contains all data generated by <seealso cref="IsolineBuilder"/>.
/// </summary>
public sealed class IsolineCollection : IEnumerable<LevelLine>
{
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<LevelLine> lines = new List<LevelLine>();
/// <summary>
/// Gets the list of isoline lines.
/// </summary>
/// <value>The lines.</value>
public List<LevelLine> 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<LevelLine> Members
public IEnumerator<LevelLine> GetEnumerator()
{
return lines.GetEnumerator();
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
}

View File

@@ -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
{
/// <summary>
/// Exception that is thrown when error occurs while building isolines.
/// </summary>
[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) { }
}
}

View File

@@ -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
{
/// <summary>
/// Draws isolines on given two-dimensional scalar data.
/// </summary>
public sealed class IsolineGraph : IsolineRenderer
{
private static Brush labelBackground = new SolidColorBrush(Color.FromArgb(130, 255, 255, 255));
/// <summary>
/// Initializes a new instance of the <see cref="IsolineGraph"/> class.
/// </summary>
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<FrameworkElement> textBlocks = new List<FrameworkElement>();
private List<Path> linePaths = new List<Path>();
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);
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More