I have a chart on my WinForm that displays an EKG signal from a vital sign monitor. The chart has a scrollbar on the X-axis that allows the user to scroll back and forth to see the signal at any point they wish. This works great. The chart gets updated in real time.
The data in the chart is displayed on form load. The problem that I have is that if the user closes and reopens the form, the scrollbar button resets itself to the far left. I want the scrollbar button to be positioned at the point in the chart that is equal to the current DateTime, but I cannot find any way to do that by looking at the chart controls. Googling has not been successful.
Here is my chart setup:
private void InitializeChartEKG() {
chartEKG.ChartAreas[0].AxisX.Title = "Time";
chartEKG.ChartAreas[0].AxisX.MajorTickMark.Enabled = true;
chartEKG.ChartAreas[0].AxisX.MinorTickMark.Enabled = true;
chartEKG.ChartAreas[0].AxisX.IsStartedFromZero = true;
chartEKG.ChartAreas[0].CursorX.LineColor = Color.LawnGreen;
chartEKG.ChartAreas[0].CursorY.LineColor = Color.LawnGreen;
chartEKG.ChartAreas[0].AxisX.MajorGrid.Enabled = true;
chartEKG.ChartAreas[0].AxisX.MajorGrid.Interval = 100;
chartEKG.ChartAreas[0].AxisX.IsStartedFromZero = true;
chartEKG.ChartAreas[0].AxisX.MajorTickMark.Enabled = true;
chartEKG.ChartAreas[0].AxisX.Minimum = 0;
chartEKG.ChartAreas[0].AxisY.Maximum = 600;
chartEKG.ChartAreas[0].AxisY.Minimum = -600;
chartEKG.ChartAreas[0].AxisX.Interval = 10000;
chartEKG.ChartAreas[0].AxisY.Title = "mV";
chartEKG.Series[0].XValueType = ChartValueType.DateTime;
chartEKG.Series[0].YValueType = ChartValueType.Int32;
chartEKG.ChartAreas[0].AxisX.IntervalType = DateTimeIntervalType.Auto;
chartEKG.ChartAreas[0].AxisX.LabelStyle.IntervalType = DateTimeIntervalType.Auto;
chartEKG.ChartAreas[0].AxisX.LabelStyle.Format = "HH:mm:ss tt";
chartArea = chartEKG.ChartAreas[0];
chartArea.AxisX.ScaleView.SizeType = DateTimeIntervalType.Auto;
int position = 0;
int blockSize = 10000;
int size = blockSize;
chartArea.AxisX.ScaleView.Zoom(position, size);
chartArea.AxisX.ScaleView.SmallScrollSize = blockSize;
chartArea.CursorX.AutoScroll = true;
chartArea.AxisX.ScrollBar.BackColor = Color.LightGray;
chartArea.AxisX.ScrollBar.ButtonColor = Color.LightSteelBlue;
chartArea.AxisX.ScrollBar.LineColor = Color.DarkBlue;
chartArea.AxisX.ScrollBar.ButtonStyle = ScrollBarButtonStyles.SmallScroll;
CultureInfo culture = new CultureInfo("en-US");
}
In the method I call to add the points I think I can do:
chartEKG.Invoke(new Action(() => chartEKG.ChartAreas[0].AxisX.ScaleView.Position = chartEKG.ChartAreas[0].AxisX.Maximum - SomeNumber);
But I'm not sure how to calculate SomeNumber
Finally was able to locate a solution. In the method that adds the points to the chart, I modified the loop that adds the points:
foreach (string point in count) {
try {
time = time.AddMilliseconds(2);
chartEKG.Invoke(new Action(() => series1.Points.AddXY(time, Convert.ToDouble(point) * 0.61)));
}
catch (ArgumentException) { }
loopCount = loopCount + 1;
string timeFormat = "yyyyMMddHHmmssfff";
string timeNow = DateTime.Now.ToString(timeFormat);
if (time == DateTime.ParseExact(DateTime.Now.ToString(timeNow), timeFormat, CultureInfo.InvariantCulture)) {
loopToStopAt = loopCount;
}
}
Then below that foreach loop, I placed this code:
chartEKG.Invoke(new Action(() => chartEKG.ChartAreas[0].AxisX.ScaleView.Scroll(chartEKG.ChartAreas[0].AxisX.Maximum - Convert.ToDouble(loopToStopAt))));
This will programmatically drag the slider to the far right of the chart, which is exactly what I want.
Related
I am trying to make a timeline. I found this How can I make a gannt chart with overlapping points in winforms , and I am using it by now like this:
void limitGantt(Chart chart, DateTime start, DateTime end)
{
Axis ax = chart.ChartAreas[0].AxisX;
ax.Minimum = 0.5;
ax.Maximum = 1.5;
Axis ay = chart.ChartAreas[0].AxisY;
ay.Minimum = start.ToOADate();
ay.Maximum = end.ToOADate();
}
void addGanttTask(Series s, DateTime start, DateTime end, Color c, int slot, string label)
{
int pt = s.Points.AddXY(slot, start, end);
s.Points[pt].Color = c;
s.Points[pt].Label = label;
s.Points[pt].LabelAngle = -90;
s.Points[pt].LabelBackColor = Color.White;
}
void setUpGantt(Chart chart, DateTime start, DateTime end)
{
chart.Series.Clear();
Series s = chart.Series.Add("gantt");
s.ChartType = SeriesChartType.RangeBar;
s.YValueType = ChartValueType.DateTime;
s.XValueType = ChartValueType.DateTime;
s.AxisLabel = "";
s.IsVisibleInLegend = false;
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
ax.MajorGrid.Enabled = false;
ax.MajorTickMark.Enabled = false;
ax.LabelStyle.Format = " ";
ax.Enabled = AxisEnabled.False;
ay.LabelStyle.Format = "HH:mm";
ay.LabelAutoFitStyle = LabelAutoFitStyles.DecreaseFont;
ay.MajorGrid.Enabled = false;
ay.MajorTickMark.Enabled = false;
ay.LabelStyle.Angle = 90;
ay.LineColor = chart.BackColor;
ay.LabelAutoFitMaxFontSize = 6;
limitGantt(chart, start, end);
}
setUpGantt(frm1.chartTimeLine, start, end);
Series s = frm1.chartTimeLine.Series[0];
addGanttTask(s, firstEvent, firstEvent, Color.DarkSlateBlue, 1, "First Event");
addGanttTask(s, secondEvent, secondEvent, Color.DarkSlateBlue, 1, "Second Event");
As you can see, I have set the start & end value as the same.
This is the result:
timeline attempt
Is not so nice... also the labels positions are not accurate (i.e. 1st event time is represented (more or less)after 11:00 when the true value is 10:55, and also if they are so close they overlap.
Does anyone know another way to make a TimeLine, or at least what I am doing wrong with the labels?
Thank you very much in advance!
By using ColorBand tool I am scoping the axis to chart, so separate chart area is assigned to that axis and signal, but when cursor is enabled then cursor is not visible for whole chart(refer attached image) and axis titles are overlapped with each other(refer attached image).
Dictionary<int, AxisScope> list = new Dictionary<int, AxisScope>();
foreach (AxisScope axis in this.Chart.Axes.Custom)
{
axis.Visible = axis.Scope;
totalWeight += axis.Weight;
while (list.Keys.Contains(axis.Ordinal))
axis.ordinal++;
list.Add(axis.Ordinal, axis);
}
int ord = 0;
double start = 0;
int pos = 0;
int[] array = list.Keys.ToArray();
Array.Sort(array);
foreach (int i in array)
{
AxisScope scope = list[i];
scope.Ordinal = ord++;
if (scope.Scope && scope.Weight > 0)
{
if (scope.AxisColorBackground == null)
scope.AxisColorBackground = new ColorBand(this.Chart);
this.Chart.Tools.Add(scope.AxisColorBackground);
scope.AxisColorBackground.Axis = scope;
Color pen = Color.DarkRed;
Color back = Color.FromArgb(253, 253, 233);
if ((pos++ % 2) == 0)
{
pen = Color.DarkBlue;
back = Color.FromArgb(233, 253, 253);
}
scope.StartPosition = start;
start += (scope.Weight / totalWeight) * 100;
scope.EndPosition = start;
scope.AxisPen.Color = pen;
scope.AxisColorBackground.Pen.Color = back;
scope.AxisColorBackground.Brush.Color = back;
scope.AxisColorBackground.Brush.Transparency = 33;
scope.AxisColorBackground.Transparency = 33;
scope.AxisColorBackground.Start = double.MinValue;// scope.Minimum;
scope.AxisColorBackground.End = double.MaxValue;// scope.Maximum;
scope.AxisColorBackground.ResizeEnd = false;
scope.AxisColorBackground.ResizeStart = false;
scope.AxisColorBackground.Tag = "Axis -" + scope.Title.ToString();
scope.AxisColorBackground.Active = true;
}
else if (scope.Scope && scope.Weight == 0)
{
scope.Visible = false;
}
}
Could you please produce a Minimal, Reproducible Example I could run here to immediately reproduce your problem? If you feel your code is too long to post here, maybe you could consider zipping up your Visual Studio project and posting it to Steema's upload page.
I have this simple graph plotted in a chart. On the X-axis the values are DateTime values.
public partial class Form1 : Form
{
List<double> valuelist = new List<double>();
List<DateTime> timelist = new List<DateTime>();
public Form1()
{
InitializeComponent();
// fill the lists with values
for (int i = 0; i < 2000; i++)
{
double value = Math.Sin(i/20.0);
valuelist.Add(value);
timelist.Add(DateTime.Now.AddMinutes(i + 2));
}
// add the Values to the chart
for (int i = 0; i < valuelist.Count; i++)
{
this.chart1.Series[0].Points.AddXY(timelist[i], valuelist[i]);
}
this.chart1.ChartAreas[0].AxisX.LabelStyle.Format = "dd.MM-hh:mm";
}
private void Form1_Load(object sender, EventArgs e)
{
chart1.Series[0].XValueType = ChartValueType.DateTime;
chart1.ChartAreas[0].AxisX.Maximum = timelist.Max().ToOADate();
chart1.ChartAreas[0].AxisX.Minimum = timelist.Min().ToOADate();
chart1.ChartAreas[0].CursorX.AutoScroll = true;
chart1.ChartAreas[0].CursorY.AutoScroll = true;
chart1.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
chart1.ChartAreas[0].CursorY.IsUserSelectionEnabled = true;
chart1.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
chart1.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
DateTime intervall = timelist.Min().AddHours(3);
chart1.ChartAreas[0].AxisX.ScaleView.Zoom(chart1.ChartAreas[0].AxisX.Minimum, intervall.ToOADate());
// disable zoom-reset button
chart1.ChartAreas[0].AxisX.ScrollBar.ButtonStyle = ScrollBarButtonStyles.SmallScroll;
// set scrollbar small change to blockSize
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollSize = intervall.ToOADate();
}
}
My problem is that I cannot get the scrollbar to move smoothly. When I plot only the Y-Values and use just double values for the AxisX.Maximum, AxisX.Minimum, AxisX.ScaleView.Zoom and for AxisX.ScaleView.SmallScrollSize it works like a charm. But as soon as I use DateTime for the X-Values I can only scroll in steps. Does someone know how to surpass this? I have the feeling that this piece of code is the obstacle:
// set scrollbar small change to blockSize (e.g. 100)
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollSize = intervall.ToOADate();
EDIT:
The Interval for the X-Axis is automatic, the range is set by the ZoomLevel of chart1.ChartAreas[0].AxisX.ScaleView.Zoom. Here is a picture:
EDIT 2:
The Values for the X-Axis are DateTime-Values simulating a sampling of 1 value every minute:
timelist.Add(DateTime.Now.AddMinutes(i + 2));
Because it is a lot of values I did not set an interval.
The code is posted this way, so that it can be copied as it is and run right away to try it out.
You scrolling interval is wrong.
It should not be the start of your data but the step you want to go when scrolling.
Looks like you want to scroll by 3 hours?
Here is what you do:
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollSizeType = DateTimeIntervalType.Hours;
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollSize = 3;
If you wanted to use a DateTime.ToOADate double to achieve the same you need it to start at the first day of the DataTime data type (0aka 'dawn of time' aka 1899-12-30) and then add 3 hours:
DateTime interval = DateTime.FromOADate(0).AddHours(3);
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollSizeType = DateTimeIntervalType.Number;
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollSize = interval.ToOADate();
To allow dragging the lift smoothly this may work better than setting the SmallScrollSize:
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollMinSizeType =DateTimeIntervalType.Minutes;
chart1.ChartAreas[0].AxisX.ScaleView.SmallScrollMinSize = 60;
Use your unit and numbers! This will only work if you don't set the SmallScrollMinSize.
I am working with a RangeBar Chart in a c# form app. When I add the series to the chart they do no line up correctly. When I change the "DrawSideBySide=false" it works fine, but I need some of the series to be side by side.
Any help on this would be greatly appreciated.
My code is just a List of Series being populated and then those series being added to a chart.
I loop through this populating series and adding them to a series list.
Populating-
double yplot1 = (double)user.Projects[i].StartDate.ToOADate();
double yplot2 = (double)user.Projects[i].EndDate.ToOADate();
// Use a different series for each datapoint
Series seriesInstance = new Series();
//seriesInstance.Name = user.Name;
seriesInstance.Label = user.Projects[i].Name + " - " + (user.Projects[i].AllocationPercent * 100).ToString() + "%";
seriesInstance.AxisLabel = user.Name;
seriesInstance.ChartType = SeriesChartType.RangeBar;
// Have a start and end date so plotting 2 points on the y-axis
seriesInstance.YValuesPerPoint = 2;
//seriesInstance.CustomProperties = "DrawSideBySide=true";
//seriesInstance["PixelPointWidth"] = "200";
seriesInstance["MinPixelPointWidth"] = "150";
int xordinal = j;
seriesInstance.IsXValueIndexed = false;
seriesInstance.Points.AddXY(xordinal, yplot1, yplot2);
/*seriesInstance.Points[0].ToolTip = someTipText;
seriesInstance.Points[0].Color = resourceColor;
seriesInstance.Points[0].AxisLabel = xlabel;*/
seriesList.Add(seriesInstance);
Then I add all the series in the list to the chart
foreach (Series plotSeries in seriesList)
{
chart1.Series.Add(plotSeries);
}
// Force x-axis to show each task or resource
chart1.ChartAreas[0].AxisX.Interval = 1;
// Set y-axis to show each day of the month
chart1.ChartAreas[0].AxisY.Interval = 1;
ChartArea chartArea1 = chart1.ChartAreas[0];
chartArea1.AxisX.IsReversed = true;
// Set other y-axis properties
chartArea1.AxisX.ScrollBar.Enabled = true;
chartArea1.AxisX.IsLabelAutoFit = true;
chartArea1.AxisX.ScaleView.Size = 5;
chartArea1.AxisY.IsStartedFromZero = false;
chartArea1.AxisY.IsMarginVisible = false;
if ((lastDate - firstDate).TotalDays < 60)
{
chartArea1.AxisY.IntervalType = DateTimeIntervalType.Days;
}
else if (((lastDate - firstDate).TotalDays > 60) && ((lastDate - firstDate).TotalDays < 365))
{
chartArea1.AxisY.IntervalType = DateTimeIntervalType.Weeks;
}
else
{
chartArea1.AxisY.IntervalType = DateTimeIntervalType.Months;
}
// Set the y-axis labels
chart1.ChartAreas[0].AxisY.Minimum = firstDate.AddDays(-1).ToOADate();
chart1.ChartAreas[0].AxisY.Maximum = lastDate.AddDays(+1).ToOADate();
chart1.ChartAreas[0].AxisY.LabelStyle.Format = "ddd M/d";
// Force redraw of chart
chart1.Update();
I have a charting application that has an overlay function which reassigns the 'from' chart series to the 'to' chart using this code :
chTo.Series.Add(chFrom.Series[s]); //Reassign series to new chart
chTo.Legends.Add(chFrom.Legends[s]); //Reassign legend to new chart
Works great. However, I am trying to implement tooltips for the legends and am running into an issue where only the first legend in the chart will show tooltips. When I do a hittest only the first legend is recognized. All subsequent legends, while visible on the chart, aren't 'seen' to the hittest method. I'm thinking this is why the tooltips aren't showing as there is no object to trigger the mouseover event for the tooltip.
I have been unable to find a way to 'expand' the legend area (as detected by the hittest method) to make this work.
Does anyone have any ideas? Thanks!
Responding to King King --
The original legend is created in the same method as the chart thus:
//Create the series legend
chartSel.Series[ySeries.Name].ChartArea = "ChartArea1";
chartSel.Legends.Remove(chartSel.Legends.FindByName("Legend1"));
chartSel.Legends.Add(ySeries.Name);
chartSel.Legends[0].Name = ySeries.Name;
//Format the series legend
chartSel.Legends[ySeries.Name].Docking = Docking.Right;
chartSel.Legends[ySeries.Name].DockedToChartArea = "ChartArea1";
chartSel.Legends[ySeries.Name].Alignment = StringAlignment.Near;
chartSel.Legends[ySeries.Name].IsDockedInsideChartArea = false;
chartSel.Legends[ySeries.Name].LegendStyle = LegendStyle.Table; //.Row;
chartSel.Legends[ySeries.Name].TableStyle = LegendTableStyle.Tall;
chartSel.Legends[ySeries.Name].IsEquallySpacedItems = false;
chartSel.Legends[ySeries.Name].Font = new Font("Segoe UI", 7, FontStyle.Bold);
//chartSel.Legends[ySeries.Name].TextWrapThreshold = 17; // 19;
chartSel.Legends[ySeries.Name].Position.Auto = false;
chartSel.Legends[ySeries.Name].Position.X = 80;
chartSel.Legends[ySeries.Name].Position.Y = 2;
chartSel.Legends[ySeries.Name].Position.Width = 18;
chartSel.Legends[ySeries.Name].Position.Height = 12;
//Format series data point value cell
chartSel.Legends[ySeries.Name].CellColumns.Add(new LegendCellColumn("", LegendCellColumnType.Text, ""));
chartSel.Legends[ySeries.Name].CellColumns[0].Alignment = ContentAlignment.MiddleLeft; //.TopLeft;
chartSel.Legends[ySeries.Name].CellColumns[0].Margins = new System.Windows.Forms.DataVisualization.Charting.Margins(10, 10, 1, 1);
chartSel.Legends[ySeries.Name].CellColumns[0].MinimumWidth = 500;
chartSel.Legends[ySeries.Name].CellColumns[0].MaximumWidth = 500;
chartSel.Legends[ySeries.Name].CellColumns[0].BackColor = Color.FromArgb(120, chartSel.Series[ySeries.Name].Color);
//Format legend cell spacer
chartSel.Legends[ySeries.Name].CellColumns.Add(new LegendCellColumn("", LegendCellColumnType.Text, ""));
chartSel.Legends[ySeries.Name].CellColumns[1].Alignment = ContentAlignment.TopLeft;
chartSel.Legends[ySeries.Name].CellColumns[1].Margins = new System.Windows.Forms.DataVisualization.Charting.Margins(0, 0, 0, 0);
chartSel.Legends[ySeries.Name].CellColumns[1].MinimumWidth = 25;
chartSel.Legends[ySeries.Name].CellColumns[1].MaximumWidth = 25;
chartSel.Legends[ySeries.Name].CellColumns[1].BackColor = Color.Black;
//Format series title cell
chartSel.Legends[ySeries.Name].CellColumns.Add(new LegendCellColumn("", LegendCellColumnType.Text, ySeries.Name));
chartSel.Legends[ySeries.Name].CellColumns[2].Alignment = ContentAlignment.MiddleLeft;
chartSel.Legends[ySeries.Name].CellColumns[2].Margins = new System.Windows.Forms.DataVisualization.Charting.Margins(0, 0, 1, 1);
chartSel.Legends[ySeries.Name].CellColumns[2].MinimumWidth = 1475; //1500;
chartSel.Legends[ySeries.Name].CellColumns[2].MaximumWidth = 1475; //1500;
chartSel.Legends[ySeries.Name].CellColumns[2].ToolTip = ySeries.Name;
After the series and legends have been reassigned (using the code in my original post) I then set the legend values based on the cursor position located by the following hittest in response to a mouse-down event:
pt = activePanel.PointToClient(Control.MousePosition);
ch = activePanel.GetChildAtPoint(pt) as Chart;
if (ch != null)
{
HitTestResult ht = ch.HitTest(e.X, e.Y, false);
if (ht.ChartElementType == ChartElementType.PlottingArea)
{
SetLegendValueText(ht, ch);
}
}
private void SetLegendValueText(HitTestResult ht, Chart ch)
{
//Get the datapoint 'x' index value
int dpIndex = 0;
if (ht != null)
{
switch (ht.ChartElementType)
{
case ChartElementType.DataPoint: //Cursor is on a series line
DataPoint dp = ht.Object as DataPoint;
if (dp != null)
{
dpIndex = ht.PointIndex;
}
break;
case ChartElementType.PlottingArea: //Cursor is somewhere in the plot area of the chart
dpIndex = (int)ht.ChartArea.CursorX.Position;
break;
}
}
//Set legend value and legend tooltip
for (int x = 0; x < ch.Legends.Count; x++) //foreach (Series s in ch.Series)
{
if (dpIndex > 0)
{
ch.Legends[x].Name = "Legend_" + x;
ch.Legends[x].CellColumns[0].Text = ch.Series[x].Points[dpIndex - 1].YValues[0].ToString();
ch.Legends[x].CellColumns[0].ToolTip = ch.Legends[x].CellColumns[0].Text;
}
}
}
So, I end up with the legends looking the way I want them, but the tooltips only show for the first legend item. I've tried to do custom items as well. With them I get the tooltips, but I lose the formatting. This has been driving me crazy for weeks (off and on) and I would really like to move on to other issues. Clearly (to me anyway), I am not doing something right simply because I don't know everything there is to know about the charts, and the MSChart Samples are of very limited benefit.
I'd be most grateful if I could be pointed in the right direction.