Locating an element on hover - WPF - C# - Dynamic Data Display - c#

In Dynamic Data Display, there is a class called MarkerPointsGraph. This derives from FrameWorkElement (through a series of other classes along the way, the most immediate parent being PointsGraphBase) and overrides the OnRenderMethod to draw a set of markers on the chart.
It does this by calling a render method in the appropriate marker type (say, Triangle, Circle and so on) once for each point rendered on the screen. I need to find a way to identify when the mouse hovers on one of these markers so that I can set the tooltip for that marker.
I have a set of methods that allow me to convert a point from the screen position to the viewport position and to the data position and back. i.e. converts a screen value to a corresponding data value or viewport value and vice versa.
I also have the tooltip opening event on this framework element and the pixel size of each of these markers. As long as the user hovers on a particular marker, I need to identify which point he has hovered on and let the markerpointsgraph set the tooltip value.
However, the transforms and methods for converting the values don't seem to be working fine, especially in the x direction. THe y - direction seems to be fine.
Below is some sample code that will explain the idea:
double selectedPointX = 0;
double selectedPointY = 0;
CoordinateTransform transformLocal = this.primaryPlotter.Transform;
if (series.SeriesDescription.YAxisAffinity == AxisAffinity_Y.Y1)
{
selectedPointX = Mouse.GetPosition(this.primaryPlotter).ScreenToViewport(transformLocal).X; //Getting the mouse positions
selectedPointY = Mouse.GetPosition(this.primaryPlotter).ScreenToViewport(transformLocal).Y;
}
else if (series.SeriesDescription.YAxisAffinity == AxisAffinity_Y.Y2 && injSecondaryAxis != null)
{
transformLocal = injSecondaryAxis.Transform;
selectedPointX = Mouse.GetPosition(this.injSecondaryAxis).ScreenToViewport(transformLocal).X;
selectedPointY = Mouse.GetPosition(this.injSecondaryAxis).ScreenToViewport(transformLocal).Y;
}
else if (series.SeriesDescription.YAxisAffinity == AxisAffinity_Y.Y3 && injTertiaryAxis != null)
{
transformLocal = injTertiaryAxis.Transform;
selectedPointX = Mouse.GetPosition(this.injTertiaryAxis).ScreenToViewport(transformLocal).X;
selectedPointY = Mouse.GetPosition(this.injTertiaryAxis).ScreenToViewport(transformLocal).Y;
}
foreach (var item in SeriesList)
{
if (item.Key == GraphKey)
{
for (int i = 0; i < item.Value.Collection.Count; i++)
{
//Calculate the size of the marker on the screen and allow for some level of inaccuracy in identifying the marker i.e anywhere within the marker is allowed.
double xlowerBound = item.Value.Collection[i].DataToViewport(transformLocal).X - series.MarkerSize;
double xUpperBound = item.Value.Collection[i].DataToViewport(transformLocal).X + series.MarkerSize;
double ylowerBound = item.Value.Collection[i].DataToViewport(transformLocal).Y - series.MarkerSize;
double yUpperBound = item.Value.Collection[i].DataToViewport(transformLocal).Y + series.MarkerSize;
//If point is within bounds
if (!(selectedPointX < xlowerBound || selectedPointX > xUpperBound || selectedPointY < ylowerBound || selectedPointY > yUpperBound))
{
strToolTip = item.Value.Collection[i].X + ", " + item.Value.Collection[i].Y; //This point is set as the tooltip
break;
}
}
break;
}
}
Here, injSecondary and injTertiary are two injected plotters that provide two vertical Y axes. They behave pretty similar to the primary chart plotter.
Is there anything wrong occurring here? For some reason, points well ahead of the actual clicked point are passing the buffer clause.

Hmm, looks like you're going about this the wrong way to me. If you dig into the source code of D3, you can open one of the marker classes and directly edit the tooltip there. Every System.Windows.Shapes element has a Tooltip property which you can assign right in the marker class. All you need to do is decide what data the tooltip holds.
Example - The CircleElementPointMarker Class :
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
namespace Microsoft.Research.DynamicDataDisplay.PointMarkers
{
/// <summary>Adds Circle element at every point of graph</summary>
public class CircleElementPointMarker : ShapeElementPointMarker {
public override UIElement CreateMarker()
{
Ellipse result = new Ellipse();
result.Width = Size;
result.Height = Size;
result.Stroke = Brush;
result.Fill = Fill;
if (!String.IsNullOrEmpty(ToolTipText))
{
ToolTip tt = new ToolTip();
tt.Content = ToolTipText;
result.ToolTip = tt;
}
return result;
}
public override void SetMarkerProperties(UIElement marker)
{
Ellipse ellipse = (Ellipse)marker;
ellipse.Width = Size;
ellipse.Height = Size;
ellipse.Stroke = Brush;
ellipse.Fill = Fill;
if (!String.IsNullOrEmpty(ToolTipText))
{
ToolTip tt = new ToolTip();
tt.Content = ToolTipText;
ellipse.ToolTip = tt;
}
}
public override void SetPosition(UIElement marker, Point screenPoint)
{
Canvas.SetLeft(marker, screenPoint.X - Size / 2);
Canvas.SetTop(marker, screenPoint.Y - Size / 2);
}
}
}
You can see in this class we have code built in to handle tooltips. The ToolTipText is a property that derives from the parent class - ShapeElementPointMarker. All you have to do is assign that property with the data that you need to show.

Use a cursorcoordinategraph on the plotter in place of the Mouse.GetPosition to get the correct screen position and avoid the ScreenToViewport transform. Also, use to DataToScreen while creating the bounds.

Related

Text scaling problem with itext7, hidden margins?

I'm trying to get this c# function from within a asp.netcore razor project using itext7 (7.1.7) to output a div containing text that scales up to within the constraints given with height and width. However, for some reason it seems there's some kind of hidden margin that will not scale the text to the boundaries of the given width and height, even though the margins are set to 0.
At the moment the maximum text i get is only half of what it should be.
Also, the paragraph, nor the div is actually set to the min width and min height.
Any help how to fix this is appreciated
public Div MakeScaledTxtDiv(string _displayTxt ,PdfFont _Pdffont, float _maxHeight,
float _maxWidth, Canvas _canvas, bool _showBorder)
{
Div nameDiv = new Div();
iText.Kernel.Geom.Rectangle posRect = new iText.Kernel.Geom.Rectangle(_maxWidth,_maxHeight);
Paragraph txtPara = new Paragraph()
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER);
if (_showBorder)
txtPara.SetBorder(new DashedBorder(1));
if (_maxHeight > 0)
txtPara.SetMaxHeight(_maxHeight);
if (_maxWidth > 0)
txtPara.SetMaxWidth(_maxWidth);
txtPara
.SetMargin(0f)
.SetPadding(0f)
.SetFont(_Pdffont)
.Add(_displayTxt);
float fontSizeToSet = 1;
float fontSizeMax = 42;
float curFontSize = fontSizeMax;
bool m_bGoodfit = false;
while (!m_bGoodfit)
{
txtPara.SetFontSize(curFontSize);
ParagraphRenderer renderer = (ParagraphRenderer)txtPara.CreateRendererSubTree().SetParent(_canvas.GetRenderer());
LayoutContext context = new LayoutContext(new LayoutArea(1, posRect));
var fits = renderer.Layout(context).GetStatus();
if (fits == LayoutResult.FULL)
{
fontSizeToSet = curFontSize;
m_bGoodfit = true;
}
else
{
curFontSize -= 1;
if (curFontSize == 1)
{
fontSizeToSet = curFontSize;
m_bGoodfit = true;
}
}
}
txtPara.SetFontSize(fontSizeToSet);
if (_maxHeight > 0)
{
nameDiv.SetMinHeight(_maxHeight)
.SetMaxHeight(_maxHeight)
.SetHeight(_maxHeight);
}
if (_maxWidth > 0)
{
nameDiv.SetMinWidth(_maxWidth)
.SetMaxWidth(_maxWidth)
.SetWidth(_maxWidth);
}
nameDiv
.SetMargin(0f)
.SetPadding(0f)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER);
if (_showBorder)
nameDiv.SetBorder(new DottedBorder(1));
nameDiv.Add(txtPara).SetVerticalAlignment(VerticalAlignment.MIDDLE);
return nameDiv;
}
What worked for me was to change the multiplier number 0.7f slightly bigger in the code below (code referenced from https://stackoverflow.com/a/57929067/5490413).
It seems as that multiplier gets bigger/smaller, the space between the text and field bottom border grows/shrinks accordingly. Maybe start with 1.2f and find one that fits.
Paragraph linePara = new Paragraph().Add(lineTxt)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER).SetBorder(new DottedBorder(1))
.SetMultipliedLeading(0.7f);
lineDiv.Add(linePara);
FYI I came across the exact issue you were facing: How to scale text within a fixed rectangle with itext7?
I tried with the answer posted there https://stackoverflow.com/a/57929067/5490413 and faced the same hidden bottom margin issue here.

C# WinForms application: dual-color column in stacked chart

I am an hobbyist .NET developer. I am developing a Windows Forms app and this picture shows want I want to use a chart control for:
Basically, I want to change the upper part of every column which is taller than a certain threshold (400 in the example above). I have searched around a bit, but no joy yet. Any ideas?
EDIT: I have a graceful workaround, but it's still frustrating not to have something available in the control itself. I have created two series, red and blue. When I add a point I check if the height is bigger than the threshold. If yes, I add the point to the red series, and I also add a point of threshold height to the blue series. If not, I add it to the blue series as is. The code can explain better than me:
private void AddPointToChart(Chart chart, int x, int y)
{
if (this.threshold < y)
{
chart.Series[1].Points.AddXY(x, y);
y = this.threshold;
}
chart.Series[0].Points.AddXY(x, y);
}
As per the original question, StackedColumn is the way to go!
And for your edit, on option (not an easy one) is to override virtual methods of Chart to add your own behaviour. I came up with an example after some experimentation:
public class ThresholdColumnChart : Chart
{
private double _threshold = 50d;
public double Threshold
{
get { return _threshold; }
set { _threshold = value; Invalidate(); }
}
public ThresholdColumnChart() : base() { }
protected override void OnCustomize()
{
base.OnCustomize();
if (Series.Count != 1)
return;
Series.Add(new Series());
foreach (var dataPoint in Series[0].Points)
{
var newDataPoint = new DataPoint();
newDataPoint.XValue = dataPoint.XValue;
newDataPoint.YValues[0] = (dataPoint.YValues[0] > _threshold ?
dataPoint.YValues[0] - _threshold : 0);
Series[1].Points.Add(newDataPoint);
if (dataPoint.YValues[0] > _threshold)
dataPoint.YValues[0] = _threshold;
}
Series[0].ChartType = SeriesChartType.StackedColumn;
Series[1].ChartType = SeriesChartType.StackedColumn;
Series[1].Color = Color.Red;
}
protected override void OnPostPaint(ChartPaintEventArgs e)
{
base.OnPostPaint(e);
if (!(e.ChartElement is ThresholdBarChart))
return;
if (Series.Count != 2)
return;
for (int i = 0; i < Series[0].Points.Count; i++)
Series[0].Points[i].YValues[0] += Series[1].Points[i].YValues[0];
Series.Remove(Series[1]);
}
}
In short the OnCustomize(...) method is called just before the chart is drawn (before OnPrePaint(...)) at which point it creates a second series to be used for drawing. The new series is constructed with the threshold in mind, and the first series is reduced not to exceed the threshold just like the example in your edit.
After the chart is done painting OnPostPaint(...) will be called and the first series will be restored to it's original values, and the "extra" series is removed.
Both OnPrePaint(...) and OnPostPaint are actually called multiple times when a chart is drawn, hence why I had to put if (!(e.ChartElement is ThresholdBarChart)) return; in; to make sure we only remove the series once the chart has finnished painting.
After adding the code and compiling once you will get a new usercontrol added to the design toolbox called "ThresholdColumnChart". You can set the threshold from the Property "Threshold" (also in design). I wouldnt trust this code with my life but it should serve as a starting point.

How to add id to drawn ellipses?

This is my code to draw a maximum of 3 ellipses on a drawing canvas, I would like to have properties to describe each object drawn (such as mass, velocity and id)
How do I create an id for each new object drawn in the loop so I can give each one different masses and velocities?
Ellipse ealswith = new Ellipse();
ealswith.Height = 70; // Setting height
ealswith.Width = 70; // setting width
for (int i = 0; i <= setOfEllipses.GetUpperBound(0); i++)
{
if (this.drawingCanvas.Children.Count > 2)
{
MessageBox.Show("Maximum number of objects have been drawn.");
break;
}
if (setOfEllipses[i] == null)
{
if (this.drawingCanvas.Children.Count < 3)
{
setOfEllipses[i] = ealswith;
drawingCanvas.Children.Add(ealswith);
break;
}
}
}
I can think of two ways to do this:-
Use the Tag property of the ellipse to store the ID, or better still, create a class that contins the information you need (mass, velocity, etc) and store that object in the Tag property.
Extend the Ellipse class and the extra properties as derived members.

ILNumerics: Get mouse coordinates with ILLinePlot

I'm using ilnumerics with C# on a simple WindowsFormsApplication and created some LinePlots in a PlotCube on an ILPanel. Like in:
How to find the 3D coordinates of a surface from the click location of the mouse on the ILNumerics surface plots?
i tried to get the translated coordinates while clicking into the PlotCube to create something similar to the data tips at MATLAB.
Instead of a ILSurface I'm using ILLinePlot and my x-Axis is of logarithmic scale.
So I adapted the method mentioned at the above link to my setting.
My problem is that I only get correct coordinates when i am clicking exactly at the LinePlot!
When clicking right next to the line or anywhere else at the PlotCube the above MouseClick method runs into a NullReferenceException.
My idea for that problem was not to use the MouseClick target to get the transformations if i didn't hit the LinePlot but to set the group to the LinePlot like as if I had clicked on it (see at my example).
But although I now get the same groups in the while-loop and no Exception debugging shows that the transformation matrices of the PlotsData group and the PlotCubeScale group differ in both cases.
If I don't hit the LinePlot all transformation matrices are just the identities.
Here comes my short example:
private void ilPanel1_Load(object sender, EventArgs e)
{
ILArray<double> A = new Double[,] {{1,4,0},{10,12,0},{100,10,0},{1000,18,0},{10000,15,0}};
var scene = new ILScene() {
new ILPlotCube(twoDMode: true){
new ILLinePlot(ILMath.tosingle(A))
}
};
scene.First<ILPlotCube>().ScaleModes.XAxisScale = AxisScale.Logarithmic;
scene.First<ILPlotCube>().MouseEnter += (_s, _a) =>
{
if (!_a.DirectionUp)
{
//onplot is a global flag which is true if the mouse is over the LinePlot
Text = "On Plot - Target: " + _a.Target.ToString();
onplot = true;
}
};
scene.First<ILPlotCube>().MouseLeave += (_s, _a) =>
{
if (!_a.DirectionUp)
{
Text = "Off Plot - Target: " + _a.Target.ToString();
onplot = false;
}
};
scene.First<ILPlotCube>().MouseClick += (s, arg) =>
{
if (!arg.DirectionUp)
return;
var group = arg.Target.Parent;
// *new part: group holds my LinePlot if I clicked on it
// else it holds the PlotCube
if (!onplot)
{
// so I search the LinePlot at set group to it
foreach (ILLineStrip strip in scene.Find<ILLineStrip>())
{
if (strip.Tag == "LinePlotLine")
{
group = strip.Parent;
}
}
}
if (group != null)
{
// walk up to the next camera node
Matrix4 trans = group.Transform;
while (!(group is ILCamera) && group != null)
{
group = group.Parent;
// collect all nodes on the path up
trans = group.Transform * trans;
}
if (group != null && (group is ILCamera))
{
// convert args.LocationF to world coords
// The Z coord is not provided by the mouse! -> choose arbitrary value
var pos = new Vector3(arg.LocationF.X * 2 - 1, arg.LocationF.Y * -2 + 1, 0);
// invert the matrix.
trans = Matrix4.Invert(trans);
// trans now converts from the world coord system (at the camera) to
// the local coord system in the 'target' group node (surface).
// In order to transform the mouse (viewport) position, we
// left multiply the transformation matrix.
pos = trans * pos;
// here I take care of the logarithmic scale of the xAxis
pos.X = (float)ILMath.pow(10.0, pos.X);
// view result in the window title
Text = "Model Position: " + pos.ToString();
}
}
};
ilPanel1.Scene = scene;
}
Why do the group.Transform-matrices differ depending on my clicking position? The while-loop is exactly the same in both cases.
I hope that anyone can understand my problem. Although searching for weeks I didn't find any answer or different ways to reach my goal which is just to show the user the Coordinates of the Points on the line where he is clicking.
Thank you very much in advance for any help.
One work around is to get the lineplot node from the mouse handler's target rather than from the scene directly. That way we always get the right transforms. Replace your logic of identifying the lineplot with the following
ILGroup plotcubeGroup = e.Target as ILGroup;
ILGroup group = plotcubeGroup.Children.Where(item => item.Tag == "LinePlot").First() as ILGroup;
if(group != null)
{
// continue with same logic as above
}
Hope it helps.

Is 141 the maximum number of WPF panel items that can be animated simultaneously?

I have designed several animated Panels in C# that all inherit from one base class that performs the actual animations on the Panel.Children. Up until recently, they've all worked perfectly... but recently, I was using one with a collection that had a large number of items in it and I started getting some strange visual errors.
When the application first loads the items, the first x number of items display correctly and then the remainder are all displayed in the first position (on top of each other). If I resize the window or select an item, they all rearrange themselves to display correctly. (Using the example code below, you will have to resize the window to see them correct themselves.)
After a lot of head scratching and debugging, I discovered that the correct values were still getting through to the animation code for each item, but the last x items were simply not being animated. Each item definitely passes through the animation code, but the last x items stay in the position set in the animated Panel.ArrangeOverride method.
In order to try to locate a problem in my code, I created a new UserControl and animated Panel class with the minimum code required to recreate the problem. Having done so, I was surprised that the problem was still reproducible. With 141 items in my example code, there was no display problem, but with any more, the problem appears.
Please excuse the long lines of code, but there is so much to display.
AnimatedStackPanel.cs class
public class AnimatedStackPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
double x = 0, y = 0;
foreach (UIElement child in Children)
{
child.Measure(availableSize);
x = Math.Max(x, availableSize.Width == double.PositiveInfinity ? child.DesiredSize.Width : availableSize.Width);
y += child.DesiredSize.Height;
}
return new Size(availableSize.Width == double.PositiveInfinity ? x : availableSize.Width, availableSize.Height == double.PositiveInfinity ? y : availableSize.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (this.Children == null || this.Children.Count == 0) return finalSize;
Size previousChildSize = new Size();
Point endPosition = new Point(0, 0);
foreach (UIElement child in Children)
{
endPosition.Y += previousChildSize.Height;
double childWidth = finalSize.Width;
child.Arrange(new Rect(0, 0, childWidth, child.DesiredSize.Height));
Point startPosition = new Point(endPosition.X, endPosition.Y + finalSize.Height);
AnimatePosition(child, startPosition, endPosition, new TimeSpan(0, 0, 1));
previousChildSize = child.DesiredSize;
}
return finalSize;
}
private void AnimatePosition(UIElement child, Point startPosition, Point endPosition, TimeSpan animationDuration)
{
DoubleAnimation xAnimation = new DoubleAnimation(startPosition.X, endPosition.X, animationDuration);
DoubleAnimation yAnimation = new DoubleAnimation(startPosition.Y, endPosition.Y, animationDuration);
TransformGroup transformGroup = child.RenderTransform as TransformGroup;
if (transformGroup == null)
{
TranslateTransform translatationTransform = new TranslateTransform();
transformGroup = new TransformGroup();
transformGroup.Children.Add(translatationTransform);
child.RenderTransform = transformGroup;
child.RenderTransformOrigin = new Point(0, 0);
}
TranslateTransform translateTransform = (TranslateTransform)transformGroup.Children[0];
translateTransform.BeginAnimation(TranslateTransform.XProperty, xAnimation, HandoffBehavior.Compose);
translateTransform.BeginAnimation(TranslateTransform.YProperty, yAnimation, HandoffBehavior.Compose);
//child.Arrange(new Rect(endPosition.X, endPosition.Y, child.DesiredSize.Width, child.DesiredSize.Height));
}
}
AnimatedListBox.xaml UserControl
<UserControl x:Class="WpfTest.Views.AnimatedListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:WpfTest.Views.Controls">
<UserControl.Resources>
<ItemsPanelTemplate x:Key="AnimatedStackPanel">
<Controls:AnimatedStackPanel />
</ItemsPanelTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource="{Binding Data}" ItemsPanel="{StaticResource AnimatedStackPanel}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" />
</Grid>
</UserControl>
AnimatedListBox.xaml.cs UserControl code behind
public partial class AnimatedListBox : UserControl
{
public AnimatedListBox()
{
InitializeComponent();
Data = new ObservableCollection<int>();
// change this 150 below to anything less than 142 and the problem disappears!!
for (int count = 1; count <= 150; count++) Data.Add(count);
DataContext = this;
}
public static DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(ObservableCollection<int>), typeof(AnimatedListBox));
public ObservableCollection<int> Data
{
get { return (ObservableCollection<int>)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
}
While experimenting, I found something else that seems strange. If I change the position of the item in the Panel.ArrangeOverride method to the following, there are more unexpected results.
child.Arrange(new Rect(endPosition.X, endPosition.Y, childWidth, child.DesiredSize.Height));
Somehow, this changes the actual (animation) end positions, ie. where the items end up being positioned. It introduces some kind of double spacing of the items. How can arranging the item in its end position before it starts its animation change its final end position? Am I missing something obvious?
Also, if you uncomment the commented line in the AnimatedStackPanel.AnimatePosition method and comment out the two lines above (ie. cut out the animation and move the items directly to the same end positions that the animations would have moved them to) then you will see the problem disappear again... this applies no matter how many items are in the collection. This is what led me to think that the problem was animation related.
One last thing that I found out is that the problem exists with or without using DataTemplates for the item data type... I'm pretty sure that it's animation related. If anyone can see what I'm doing wrong, or find a solution for this, I'd be very grateful because this one has had me baffled. Can anyone even reproduce this problem with the example code? Many thanks in advance.
Sheridan, i played around with your code sample a bit and what i found out is that it apparently matters when you apply the RenderTransform. Instead of assigning it in AnimatePosition, i.e. during the first run of ArrangeOverride, i moved the code to MeasureOverride:
protected override Size MeasureOverride(Size availableSize)
{
double x = 0, y = 0;
foreach (UIElement child in Children)
{
TransformGroup transformGroup = child.RenderTransform as TransformGroup;
if (transformGroup == null)
{
TranslateTransform translatationTransform = new TranslateTransform();
transformGroup = new TransformGroup();
transformGroup.Children.Add(translatationTransform);
child.RenderTransform = transformGroup;
child.RenderTransformOrigin = new Point(0, 0);
}
child.Measure(availableSize);
x = Math.Max(x, availableSize.Width == double.PositiveInfinity ? child.DesiredSize.Width : availableSize.Width);
y += child.DesiredSize.Height;
}
return new Size(
availableSize.Width == double.PositiveInfinity ? x : availableSize.Width,
availableSize.Height == double.PositiveInfinity ? y : availableSize.Height);
}
Now the animations behave as expected even on the first invocation of ArrangeOverride. Seems like the last few RenderTransforms were not yet attached to the control while already beeing animated.
By the way, the maximum number was 144 for me on two different systems (dual core and quad core) running 32-bit Windows 7.

Categories

Resources