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:
Related
I am trying to reproduce the operation of the Control Expander WPF, or as shown in the menu of Outlook, Vertical Web Menu etc., since in WindowsForms this control does not exist. Here I leave the sample code: Menu_Expader.zip link GoogleDrive.
I have managed to do it using the following controls:
Panels
FlowLayoutPanel
1 Time Control
Button Vectors
Labels Vectors ...
This works perfectly, but it happens that to each panel I must establish a
Maximum Size and Minimum Size therefore every time I add an item inside I must modify the size of the panel where I add it, and the item are very close to each other is a bit annoying for the user's vision.
Example this is what I currently have:
EDIT
Code Sample:
// The state of an expanding or collapsing panel.
private enum ExpandState
{
Expanded,
Expanding,
Collapsing,
Collapsed,
}
// The expanding panels' current states.
private ExpandState[] ExpandStates;
// The Panels to expand and collapse.
private Panel[] ExpandPanels;
// The expand/collapse buttons.
private Button[] ExpandButtons;
// Initialize.
private void Form1_Load(object sender, EventArgs e)
{
// Initialize the arrays.
ExpandStates = new ExpandState[]
{
ExpandState.Expanded,
ExpandState.Expanded,
ExpandState.Expanded,
};
ExpandPanels = new Panel[]
{
panModule1,
panModule2,
panModule3,
};
ExpandButtons = new Button[]
{
btnExpand1,
btnExpand2,
btnExpand3,
};
// Set expander button Tag properties to give indexes
// into these arrays and display expanded images.
for (int i = 0; i < ExpandButtons.Length; i++)
{
ExpandButtons[i].Tag = i;
ExpandButtons[i].Image = Properties.Resources.expander_down;
}
}
// Start expanding.
private void btnExpander_Click(object sender, EventArgs e)
{
// Get the button.
Button btn = sender as Button;
int index = (int)btn.Tag;
// Get this panel's current expand
// state and set its new state.
ExpandState old_state = ExpandStates[index];
if ((old_state == ExpandState.Collapsed) ||
(old_state == ExpandState.Collapsing))
{
// Was collapsed/collapsing. Start expanding.
ExpandStates[index] = ExpandState.Expanding;
ExpandButtons[index].Image = Properties.Resources.expander_up;
}
else
{
// Was expanded/expanding. Start collapsing.
ExpandStates[index] = ExpandState.Collapsing;
ExpandButtons[index].Image = Properties.Resources.expander_down;
}
// Make sure the timer is enabled.
tmrExpand.Enabled = true;
}
// The number of pixels expanded per timer Tick.
private const int ExpansionPerTick = 7;
// Expand or collapse any panels that need it.
private void tmrExpand_Tick(object sender, EventArgs e)
{
// Determines whether we need more adjustments.
bool not_done = false;
for (int i = 0; i < ExpandPanels.Length; i++)
{
// See if this panel needs adjustment.
if (ExpandStates[i] == ExpandState.Expanding)
{
// Expand.
Panel pan = ExpandPanels[i];
int new_height = pan.Height + ExpansionPerTick;
if (new_height >= pan.MaximumSize.Height)
{
// This one is done.
new_height = pan.MaximumSize.Height;
}
else
{
// This one is not done.
not_done = true;
}
// Set the new height.
pan.Height = new_height;
}
else if (ExpandStates[i] == ExpandState.Collapsing)
{
// Collapse.
Panel pan = ExpandPanels[i];
int new_height = pan.Height - ExpansionPerTick;
if (new_height <= pan.MinimumSize.Height)
{
// This one is done.
new_height = pan.MinimumSize.Height;
}
else
{
// This one is not done.
not_done = true;
}
// Set the new height.
pan.Height = new_height;
}
}
// If we are done, disable the timer.
tmrExpand.Enabled = not_done;
}
I want to get a result similar to this - Bootstrap Menu Accordion:
Imitate that operation panels expand according to the quantity of item that it contains as long as it does not protrude from the screen, in which case it will show the scroll bar. I know there are software that provide custom controls like DVexpress, DotNetBar Suite among others, but they are Licensed Software I do not want to use it illegally pirate. Can you help me optimize it or create it in another way?
Environment: Visual Studio 2010 & .NET NetFramework 4.
The original question I made it in StackOverFlow in Spanish.
Modulo (Module)
Menu Principal (Main menu)
Mantenimientos (Maintenance)
Procesos (Processes)
Consultas (Queries)
Reportes (Reports)
Note: If someone speaks Spanish and English and can do a better translation, please edit the question. (Excuse the advertising on the image, I recorded the screen with a software trial version).
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!
Updated the code to give you a better idea of what's happening and to allow the data to load faster.
I have still not seen anything posted online that answers this question.
I have a MS Chart control on a C# application (Visual Studio 2010 Express, if that makes a difference). Data loads at a slow realtime rate (once per second). My intent is to display 6 minutes of data at a time, with the ability to scroll to another page of data if necessary. I want the data to fill the chart from the left hand side. As the data is added to the chart, the scroll bar and X axis legends also move over so that I can only see the latest data point. I have to scroll to the left to see earlier data -- and the the scroll bar jumps back when the new data point is added. I want the scroll bar to stay in one place (left edge) unless I move it. I have a 1 second timer on my form and new data is added with every time cycle.
I hope this is sufficient. Any help?
Code to Initialize the Chart control:
DateTime startTime = DateTime.Now;
DateTime endTime = startTime.AddMinutes(6);
DateTime maxTime = startTime.AddMinutes(24);
// Bind the chart to the list.
chartAssociateProductivity.DataSource = Globals.listKohlsPerformanceDataSource;
chartAssociateProductivity.ChartAreas["ChartArea1"].CursorX.AutoScroll = true; // enable autoscroll
chartAssociateProductivity.ChartAreas["ChartArea1"].CursorX.IsUserEnabled = false;
chartAssociateProductivity.ChartAreas["ChartArea1"].CursorX.IsUserSelectionEnabled = false;
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.Minimum = startTime.ToOADate();
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.Maximum = endTime.ToOADate();
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.ScaleView.Zoomable = true;
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.ScrollBar.IsPositionedInside = true;
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.ScaleView.Zoom(startTime.ToOADate(), 6, DateTimeIntervalType.Minutes);
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.ScaleView.Position = 0;// startTime.ToOADate();
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.MinorTickMark.Enabled = true;
// disable zoom-reset button (only scrollbar's arrows are available)
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.ScrollBar.ButtonStyle = ScrollBarButtonStyles.SmallScroll;
// set scrollbar small change to blockSize (e.g. 100)
chartAssociateProductivity.ChartAreas["ChartArea1"].AxisX.ScaleView.SmallScrollSize = 100;
Code to Add the Data (for the example, I will add constant data):
private void timer1_Tick(object sender, EventArgs e)
{
listPerformanceDataSource.Add(new PerformanceRecord(0, 248));
}
The data structure:
public class PerformanceRecord
{
int bagCount, goal;
public PerformanceRecord(int bagCount, int goal)
{
this.bagCount = bagCount;
this.goal = goal;
}
public int BagCount
{
get { return bagCount; }
set { bagCount = value; }
}
public int Goal
{
get { return goal; }
set { goal = value; }
}
}
// Create a list.
public static List<PerformanceRecord> listPerformanceDataSource = new List<PerformanceRecord>();
ChartArea chartArea=new ChartArea();
chartArea = chart1.ChartAreas[series.ChartArea];
int scrollBarVal=chartArea.AxisX.ScaleView.Position;
you have to make a chartArea instance and use its axis position
I am working on a Dashboard System where i am using Line Chart in WinForms. I need to show the tooptip on each line. I have tried this
var series = new Series
{
Name = chartPoint.SetName,
Color = chartPoint.ChartColor,
ChartType = SeriesChartType.Line,
BorderDashStyle = chartPoint.ChartDashStyle,
BorderWidth = chartPoint.BorderWidth,
IsVisibleInLegend = !chartPoint.HideLegend,
ToolTip = "Hello World"
};
but its not working for me
You have two options either use Keywords accepted by the Chart control.
myChart.Series[0].ToolTip = "Name #SERIESNAME : X - #VALX{F2} , Y - #VALY{F2}";
In the Chart control, a Keyword is a character sequence that is replaced with an automatically calculated value at run time. For a comprehensive list of keywords accepted by the Chart control look up Keyword reference
or
if you want something more fanciful, you have to handle the event GetToolTipText
this.myChart.GetToolTipText += new System.Windows.Forms.DataVisualization.Charting.Chart.ToolTipEventHandler(this.myChart_GetToolTipText);
Now I am not sure what you want to show on the ToolTip but you could add the logic accordingly. Assuming you want to show the values of the DataPoints in the series
private void myChart_GetToolTipText(object sender, System.Windows.Forms.DataVisualization.Charting.ToolTipEventArgs e)
{
switch(e.HitTestResult.ChartElementType)
{
case ChartElementType.DataPoint:
e.Text = myChart.Series[0].Points[e.HitTestResult.PointIndex]).ToString()
+ /* something for which no keyword exists */;
break;
case ChartElementType.Axis:
// add logic here
case ....
.
.
default:
// do nothing
}
After some RnD i got tooltips on Line Series, but still confused why its not working with this solution.
Here is the solution
series.ToolTip = string.Format("Name '{0}' : X - {1} , Y - {2}", chartPoint.SetName, "#VALX{F2}",
"#VALY{F2}");
mainChartControl.GetToolTipText += ChartControlGetToolTipText;
private void ChartControlGetToolTipText(object sender, ToolTipEventArgs e)
{
}
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.