Just a quick one.
I have a graph that has the possibility to display 9 different series, data comes in through textboxes from user and populates these respective series.
The graph is linked to a checkedlistbox and the items that are checked in the listbox enable their respective series on the chart. Only 2 series may be enabled at any one time, which works without a problem using the code below:
private void chListBoxChartSeries_ItemCheck(object sender, ItemCheckEventArgs e)
{
if (e.NewValue == CheckState.Checked && chListBoxChartSeries.CheckedItems.Count >= 2)
{
e.NewValue = CheckState.Unchecked;
}
}
public void saveChartSeries()
{
//placeholder variable to relate between checklist item and chart series
string seriesName;
for (int index = 0; index < chListBoxChartSeries.Items.Count; ++index)
{
seriesName = chListBoxChartSeries.Items[index].ToString();
if (chListBoxChartSeries.CheckedItems.Contains(chListBoxChartSeries.Items[index]))
{
main.chartVitals.Series[seriesName].Enabled = true;
}
else
{
main.chartVitals.Series[seriesName].Enabled = false;
}
}
}
There's one thing I do want to do following this, I would like whichever series are enabled to be set to a colour each (first series red, second series blue for example). I'm struggling to find an efficient way to do this, but I imagine it involves setting the first of the two indexes to one colour (red) and the second to another colour (blue). I figure I can do this using the existing for-loop in the saveChartSeries() function, something like this:
public void saveChartSeries()
{
//placeholder variable to relate between checklist item and chart series
string seriesName;
for (int index = 0; index < chListBoxChartSeries.Items.Count; ++index)
{
seriesName = chListBoxChartSeries.Items[index].ToString();
if (chListBoxChartSeries.CheckedItems.Contains(chListBoxChartSeries.Items[index]))
{
main.chartVitals.Series[seriesName].Enabled = true;
if (main.chartVitals.Series[seriesName].Enabled == true)
{
//set series color to Color.Red
//if there is already a red series, set to Color.Blue
}
}
else
{
main.chartVitals.Series[seriesName].Enabled = false;
}
}
}
This is about as much as I can get so far, if anyone can offer a next step, or if I'm over-complicating it and there's a simpler way, I'd really appreciate someone pointing it out!
If I understand you correctly, you want to color each visible series with a color from a fixed list.
That will involve changing subsequent series' colors whenever you enable or disable a previous series, right?
Here is a function that will do that:
void colorSeries(Chart chart)
{
List<Color> seriescolors = new List<Color>
{ Color.Khaki, Color.Brown, Color.CornflowerBlue,
Color.DarkCyan, Color.ForestGreen, Color.Gold, Color.HotPink, Color.Indigo};
int co = 0;
foreach (Series s in chart.Series)
if (s.Enabled) s.Color = seriescolors[co++];
}
You would call it each time you enable or disable a Series.
You also wrote: if I'm over-complicating it and I figure I can do this using the existing for-loop. Hmm. In my opion you are both overcomplicating it and also setting a completely wrong priority.
Do not try to fit something in an 'existing loop'; instead keep things simple and call a function to take care of the display colors after you have processed the user actions.
Try to 'separate concerns', and always aim at creating small and self-sufficient routines!
Related
I have an app that communicates via BLE to an array of Arduino temperature sensors. I get the data and I normally display it as a table, however I've been playing around in trying to get a line-chart done but It seems I can't get "multiple" lines going. There is a Probearray that has 50 values, so I expect to have 50-lines (albeit most of them should be at the same "temperature" so I should see them overlap, however I have 2 values that the sensor isnt connected so they show really off values, but instead of showing as different lines, they show as part of the same series).
Here's the button code to "open" the graphing window:
private void graphButton_Click(object sender, EventArgs e)
{
graphView.Show();
isGraphing = true;
if (graphView.DialogResult == DialogResult.OK)
{
isGraphing = false;
}
}
Here's the code that is called out every time I get a new "reading" from the Bluetooth device, the reading triggers the "update" of every textbox in the control panel so basically they have the "latest" values:
private void passGraphData()
{
foreach (Control c in sensorTempPanel.Controls)
{
for (int i = 0; i < probeArray.Length; i++)
{
if (c.Name != "" && c.Name.Length > 2)
{
if (probeArray[i].Name == c.Name.Substring(c.Name.Length - 2))
{
this.Invoke((MethodInvoker)delegate { graphView.addGraphData(c.Name.Substring(c.Name.Length - 2), DateTime.Now, Convert.ToSingle(c.Text)); });
}
}
}
}
}
This is the code that's on the graph-view window that I believe should generate its own series per-data value (so if there are 50, each with an individual name there should be 50 series total).
internal void addGraphData(string series, DateTime xPoint, float yPoint)
{
//Every time you parse a new value for each "selected" point you add it individually by sending the series (ProbeLocation), the DateTime and Temp from the TempReadings.
if (lineChart.Series.IndexOf(series) == -1)
{
lineChart.Series.Add(series); //If it doesnt exist, create it.
//lineChart.Series[series]. <- CONFIG STUFF?
}
this.Invoke((MethodInvoker)delegate
{
lineChart.Invalidate();
xValues.Add(xPoint);
yValues.Add(yPoint);
lineChart.Series[series].ChartType = SeriesChartType.Line;
lineChart.Series[series].Points.DataBindXY(xValues, yValues);
});
}
** GOAL DESCRIPTION **
So, my intent is to get 50-lines, I understand (based on the values I see on the tables) I should be getting 2 straight lines with a value of 150 and 1 straight line with a value of -40. However (these are the sensors with some wiring issues) the remaining 47 sensors should be reading somewhere in the 20's (room temperature).
I think my issue is that I am not creating an individual "xValues and yValues" for each series, but I am not sure how to re-use these. . . I understand there's Points.DataBindXY, but is there a reverse of this? (like a Points.GetX and Points.GetY)
However, when I click on the chart button this is my output:
I am currently using WinForms. I have two Lists of Values (doubles) which I visualize with the NuGet package "LiveCharts". Now to my problem: The list of the values constantly changes as new values are added to both lists with a frequency of up 200 values / second. Right now I am doing this with Random Values. In a loop I add a new random value to each list and remove the value with the index 0. Now I want the chart to update everytime a new value is added to the list, but although I call the method which should do that everytime I change the values, the chart only updates after the loop ended.
This method is called by a button and should add 10 new values. This works, but the chart only updates after the for-loop is finished, although the UpdateChart() method is called everytime.
private void ReloadButton_Click(object sender, EventArgs e)
{
for (int i = 0; i < 10; i++)
{
valueList1.RemoveAt(0);
AddRandomValueToList1();
valueList2.RemoveAt(0);
AddRandomValueToList2();
UpdateChart();
}
}
This is the UpdateChart() method I use to update the chart Revenue is a class object and contains a value and a DateTime
private void UpdateChart()
{
series1 = new SeriesCollection();
// series.Clear();
double[] values1 = new double[valueList1.Count];
double[] values2 = new double[valueList2.Count];
int index = 0;
foreach(Revenue current in valueList1)
{
values1[index] = current.value;
index++;
}
index = 0;
foreach (Revenue current in valueList2)
{
values2[index] = current.value;
index++;
}
series1.Add(new LineSeries()
{
Title = "Values1",
Values = new ChartValues<double>(values1)
});
series1.Add(new LineSeries()
{
Title = "Values2",
Values = new ChartValues<double>(values2)
});
cartesianChart1.Series = series1;
}
I have already tried to add breaks (Thread.Sleep) hoping the chart would refresh, but that did not work. As well as the cartesianChart1.Refresh() method which I called in the for-loop. Also did I disable the animations, but still got not the result I was hoping achieve.
Now I do not know what I can do to force the chart to refresh everytime I change the values of the lists in order to create a realtime chart.
I am grateful for every help
Thanks in advance
I am using the Chart class in Visual Studio 2013 to visualize some of my data. However, my data quickly spawns many series and it's very important to have them all in one chart. I limited the legend area to 20% of the complete chart area, and so I pretty much cannot display more than 7-8 legend items when I stretch my chart to its maximum size. The control just puts ... after it runs out of space for legend items.
Instead of it just writing ..., is it somehow possible to add a scrollbar to the legend and be able to see all of the items? I am aware that I can implement my own legend in some way, but I would like to squeeze the most out of what the Chart class has to offer. I would also like to add checkboxes next to each legend item which would indicate whether the series should be hidden on the chart or not. Is this possible to do without my own legend implementation?
Additionally, I would also like to have a menu expand on right click on a legend item with a few options, but that's completely optional. Scrollbar and checkboxes are my main problem now.
Thanks.
General idea: You have to create two charts. One is main and second for legend only. You will have same series style if series order will be the same.
For showing pop up on right click on legend item:
Connect ContextMenu (ContextMenuStrip class in toolbox) to your legend's chart.
For showing hiding series from legend:
You have to implement MouseClick event handler and check what object is under mouse cursor using math (GetChildAtPoint() method doesn't work for legend items). Equation: is series_index = control_relative_mouse_y / c_legendItemHeight where c_legendItemHeight is value you provide to compute controls height (height of single legend item).
You have to configure your legend chart to contain LegendStyle to Row, MaximumAutoSize to 100, Docking to Left, IsTextAutoFit to false and IsEquallySpacedItems to true.
You have define 3 columns in your legend (one for series style, second for checkbox and third for series name). Use series CustomProperties to keep visibility state. In check column use this custom property (Text = "#CUSTOMPROPERTY(...)") to show check state. Chart does not support auto sizing. You can do it manually. During series load set your chart height to calculated value. This value equals to _stock.Shares.Count * c_legendItemHeight + 9. Where: _stock.Shares.Count is number of items in legend, c_legendItemHeight constant height of item (integer value, numbers grater then 18 seems to work for me), 9 (seems to be constant). I know it is not nice but I cannot find any better solution. I've added 502 series in my example and it worked fine. Make sure that you don't have any margin in your chart because otherwise you will be not able to calculate series number correctly.
For "many series in legend" problem:
Put your legend chart into a panel with AutoScroll property turned on. Set panels and legends height using expression from above description.
Source code:
public partial class Form1 : Form
{
private const int c_legendItemHeight = 20;
private const string c_checkCustomPropertyName = "CHECK";
private const string c_checkedString = "✔"; // see http://www.edlazorvfx.com/ysu/html/ascii.html for more
private const string c_uncheckedString = "✘";
private Stock _stock;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
_stock = Stock.Load();
// mainChart
mainChart.Legends.Clear();
foreach (Share share in _stock.Shares)
{
Series series = mainChart.Series.Add(share.Name);
series.ChartType = SeriesChartType.Line;
foreach (ShareQuotation shareQuotation in share.Quotations)
{
series.Points.AddXY(shareQuotation.Date.ToString(), shareQuotation.Close);
}
}
// LegendChart
Legend legend = legendChart.Legends[0];
legendChart.Series.Clear();
legend.IsTextAutoFit = false;
legend.IsEquallySpacedItems = true;
legend.MaximumAutoSize = 100;
legend.Docking = Docking.Left;
legend.LegendStyle = LegendStyle.Column;
legend.Position.Auto = true;
legend.Position.Width = 100;
legend.Position.Height = 100;
legend.CellColumns[1].Text = "#CUSTOMPROPERTY(" +c_checkCustomPropertyName+ ")";
foreach (Share share in _stock.Shares)
{
Series series = legendChart.Series.Add(share.Name);
series.SetCustomProperty(c_checkCustomPropertyName,c_checkedString);
}
legendChart.Height = _stock.Shares.Count * c_legendItemHeight + 9; // 9 - seems to be constant value
legendPanel.Height = legendChart.Height;
}
private void legendChart_MouseClick(object sender, MouseEventArgs e)
{
Point mousePosition = legendChart.PointToClient(Control.MousePosition);
int seriesNo = mousePosition.Y / c_legendItemHeight;
Series series = legendChart.Series[seriesNo]; // TODO - check if not out of range
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
// check uncheck series
if (series.GetCustomProperty(c_checkCustomPropertyName) == c_checkedString)
{
// if checked
// uncheck
series.SetCustomProperty(c_checkCustomPropertyName, c_uncheckedString);
series.CustomProperties = series.CustomProperties; // workaround - trigger change - is this a bug?
// hide in mainChart
mainChart.Series[seriesNo].Enabled = false;
}
else
{
// if unchecked
legendChart.Series[seriesNo].SetCustomProperty(c_checkCustomPropertyName, c_checkedString);
series.CustomProperties = series.CustomProperties; // workaround - trigger change - is this a bug?
// show in mainChart
mainChart.Series[seriesNo].Enabled = true;
}
}
}
private void contextMenu_Opening(object sender, CancelEventArgs e)
{
Point mousePosition = legendChart.PointToClient(Control.MousePosition);
int seriesNo = mousePosition.Y / c_legendItemHeight;
Series series = legendChart.Series[seriesNo]; // TODO - check if not out of range
contextMenu.Items.Clear();
string state = series.GetCustomProperty(c_checkCustomPropertyName) == c_checkedString ? "visible" : "hidden";
contextMenu.Items.Add("&Some strange action for " + state + " item named " + series.Name);
contextMenu.Items.Add("&Another action ...");
}
}
Result should look like this:
Using Infragistics UltraGrid in WinForms in C#:
I am conditionally changing the color of the ForeColor of some GroupByRows on a grid. When the user clicks the row, the color changes back to the active/selected/hot tracked/whatever color until they click on something else. I'd like the text color of the rows that I have conditionally colored to never change. Here's how I'm setting the color:
Row.Appearance.ForeColor = System.Drawing.Color.Orange;
Any idea how to make it stick even when the row is clicked?
Thanks!
This can be done with a draw filter to set the fore color of the description since this would apply at all times.
A simple example is the following draw filter which will make all group by rows that have an integer value that is even orange:
public class GroupByRowDrawFilter:IUIElementDrawFilter
{
public bool DrawElement(DrawPhase drawPhase, ref UIElementDrawParams drawParams)
{
GroupByRowDescriptionUIElement element = (GroupByRowDescriptionUIElement)drawParams.Element;
if (element.GroupByRow.Value is int)
{
int value = (int)element.GroupByRow.Value;
if (value % 2 == 0)
{
drawParams.AppearanceData.ForeColor = Color.Orange;
}
}
return false;
}
public DrawPhase GetPhasesToFilter(ref UIElementDrawParams drawParams)
{
if (drawParams.Element is GroupByRowDescriptionUIElement)
return DrawPhase.BeforeDrawElement;
return DrawPhase.None;
}
}
To apply the draw filter use the following line of code:
this.ultraGrid1.DrawFilter = new GroupByRowDrawFilter();
Note that this approach requires that the condition be in the draw filter. If this doesn't work for you, you could modify your logic where you are currently setting the ForeColor to set the Tag property of the GroupByRow instead and then check the Tag property in the draw filter to determine if you need to apply your logic.
I think you should also change the
grd.DisplayLayout.Override.SelectedRowAppearance.ForeColor = System.Drawning.Color.Orange;
or better
grd.DisplayLayout.Override.GroupByRowAppearance.ForeColor = System.Drawning.Color.Orange;
sorry, but I'm away from a PC where I can test.
Usually these properties could be changed effectively in the InitializeLayout event where you get the Layout object inside the event arguments.
e.Layout.Override.GroupByRowAppearance.ForeColor = Color.Orange;
EDIT: At the moment the only solution that I have found is the following
private void grd_BeforeRowActivate(object sender, RowEventArgs e)
{
// You need to add the additional logic required by you to
// determine which rows need to have the forecolo changed...
if (e.Row.IsGroupByRow == true)
grd.DisplayLayout.Override.ActiveRowAppearance.ForeColor = Color.Orange;
else
grd.DisplayLayout.Override.ResetActiveRowAppearance();
}
Infragistics says:
the "Ability to have active and selected conditional appearances for the GroupByRows" has been determined to be a new product idea
I'm displaying a set of search results in a ListView. The first column holds the search term, and the second shows the number of matches.
There are tens of thousands of rows, so the ListView is in virtual mode.
I'd like to change this so that the second column shows the matches as hyperlinks, in the same way as a LinkLabel shows links; when the user clicks on the link, I'd like to receive an event that will let me open up the match elsewhere in our application.
Is this possible, and if so, how?
EDIT: I don't think I've been sufficiently clear - I want multiple hyperlinks in a single column, just as it is possible to have multiple hyperlinks in a single LinkLabel.
You can easily fake it. Ensure that the list view items you add have UseItemStyleForSubItems = false so that you can set the sub-item's ForeColor to blue. Implement the MouseMove event so you can underline the "link" and change the cursor. For example:
ListViewItem.ListViewSubItem mSelected;
private void listView1_MouseMove(object sender, MouseEventArgs e) {
var info = listView1.HitTest(e.Location);
if (info.SubItem == mSelected) return;
if (mSelected != null) mSelected.Font = listView1.Font;
mSelected = null;
listView1.Cursor = Cursors.Default;
if (info.SubItem != null && info.Item.SubItems[1] == info.SubItem) {
info.SubItem.Font = new Font(info.SubItem.Font, FontStyle.Underline);
listView1.Cursor = Cursors.Hand;
mSelected = info.SubItem;
}
}
Note that this snippet checks if the 2nd column is hovered, tweak as needed.
Use ObjectListView -- an open source wrapper around a standard ListView. It supports links directly:
This recipe documents the (very simple) process and how you can customise it.
The other answers here are great, but if you don't want to have to hack some code together, look at the DataGridView control which has support for LinkLabel equivalent columns.
Using this control, you get all the functionality of the details view in a ListView, but with more customisation per row.
You can by inheriting the ListView control override the method OnDrawSubItem.
Here is a VERY simple example of how you might do:
public class MyListView : ListView
{
private Brush m_brush;
private Pen m_pen;
public MyListView()
{
this.OwnerDraw = true;
m_brush = new SolidBrush(Color.Blue);
m_pen = new Pen(m_brush)
}
protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e)
{
e.DrawDefault = true;
}
protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e)
{
if (e.ColumnIndex != 1) {
e.DrawDefault = true;
return;
}
// Draw the item's background.
e.DrawBackground();
var textSize = e.Graphics.MeasureString(e.SubItem.Text, e.SubItem.Font);
var textY = e.Bounds.Y + ((e.Bounds.Height - textSize.Height) / 2);
int textX = e.SubItem.Bounds.Location.X;
var lineY = textY + textSize.Height;
// Do the drawing of the underlined text.
e.Graphics.DrawString(e.SubItem.Text, e.SubItem.Font, m_brush, textX, textY);
e.Graphics.DrawLine(m_pen, textX, lineY, textX + textSize.Width, lineY);
}
}
You can set HotTracking to true so that when the user hovers mouse over the item it appears as link.