I have a 6300 * 5 array with:
Columns 1,2 = CIE data
Columns 3,4,5 = S R G B
How should I Draw this in MsChart?
You have several options:
Add DataPoints with Markers in the respective Colors
Add Annotations
Use one of the xxxPaint events
With only 6500 points you can't really fill the area by setting single pixels. So you better use a FillElipse call for each point.
If you use the Pre- or PostPaint event you will need to use the AxisX/Y methods ValueToPixelPosition for calculating the pixel coordinates from the CIE values.
In any case you set the Minimum and Maximum for both Axes.
Also you will need to calculate either the Markers' or the Annotations' or the ellipses' size from the chart's ClientSize to avoid ugly gaps in the colored area.
If you want to use DataPoints set the ChartType = Point and use this function for each of your data:
DataPoint Cie2DataPoint(float x, float y, float r, float g, float b)
{
var dp = new DataPoint(x, y);
dp.Color = Color.FromArgb((int)(256 * r), (int)(256 * g),(int)(256 * b));
dp.MarkerColor = dp.Color;
return dp;
}
Here are examples of helper function:
int MarkerSize(Chart chart, int count)
{
return Math.Max(chart.ClientSize.Width, chart.ClientSize.Height )/ count + 1
}
void Rescale(Chart chart)
{
Series s = chart3.Series[0];
s.MarkerSize = MarkerSize(chart3, (int)Math.Sqrt(s.Points.Count));
}
The former takes an estimate of how many plot points you expect per axis; you may need to experiment a little. The next one assumes the points are actually filling a square; also that you only have one ChartArea.
This should also be modified for your data!
We need to rescale the sizes when the Chart is resized:
private void chart3_Resize(object sender, EventArgs e)
{
Rescale (sender as Chart);
}
Here is an example of setting it up with a calculated set of data. You should loop over your list of data instead..:
Series s = chart3.Series[0];
s.ChartType = SeriesChartType.Point;
s.MarkerSize = 3;
for (int x = 0; x < 100; x++)
for (int y = 0; y < 100; y++)
{
s.Points.Add(Cie2DataPoint(x/100f, y/100f, x/100f, y/100f, (x+y)/200f));
}
ChartArea ca = chart3.ChartAreas[0];
ca.AxisX.Minimum = 0;
ca.AxisY.Minimum = 0;
ca.AxisX.Maximum = 1;
ca.AxisY.Maximum = 1;
ca.AxisX.Interval = 0.1f;
ca.AxisY.Interval = 0.1f;
ca.AxisX.LabelStyle.Format = "0.00";
ca.AxisY.LabelStyle.Format = "0.00";
Rescale(chart3);
Result:
After grabbing ~6k colors from a CIE color chart the result looks rather grainy but basically correct:
Note that you probably need to allow for the reversed y-axis somehow; I simply subtracted my y-values from 0.9f. Use your own numbers!
Related
I'm currently working on a project, in which I need to create a Polar Plot with dynamically generated data. I've managed to create a somewhat decent polar plot, but have not been able to create what is needed.
This is my Polar Plot
this is the code I used to set the offset in the middle:
public Form1()
{
InitializeComponent();
chart1.ChartAreas[0].AxisX.MajorTickMark.Enabled = false;
chart1.ChartAreas[0].AxisX.MajorGrid.Enabled = false;
chart1.ChartAreas[0].AxisY.Minimum = -20;
chart1.ChartAreas[0].AxisY.MajorGrid.IntervalOffset = 15;
chart1.ChartAreas[0].AxisY.MajorGrid.Interval = 5;
chart1.ChartAreas[0].AxisY.MajorGrid.LineDashStyle = ChartDashStyle.Solid;
chart1.ChartAreas[0].AxisX.MajorGrid.LineDashStyle = ChartDashStyle.Solid;
}
I found some help here: How to displace the origin of the Y axis on a polar Mschart?
I got a example on how I'm trying to get the polar:
The finished example
I don't think you can make an axis start from anywhere but its minimum.
(The linked post only makes the labels start from a different value.)
So we'll have to help with a little bit of owner-drawing.
A few short references:
var ca = chart1.ChartAreas[0];
var ax = ca.AxisX;
var ay = ca.AxisY;
Now let's hide the y-axis:
ay.LineWidth = 0;
To draw the portion of the axis from the interval offset to the maximum we simply code the PostPaint event:
private void chart1_PostPaint(object sender, ChartPaintEventArgs e)
{
// add references..
..
// then use values to calulate pixel coordinates..
int py1 = (int)ay.ValueToPixelPosition(ay.Minimum + ay.IntervalOffset);
int py2 = (int)ay.ValueToPixelPosition(ay.Maximum);
int px = (int)ax.ValueToPixelPosition(ax.Maximum - ax.Minimum);
// blue to make it stand out
e.ChartGraphics.Graphics.DrawLine(Pens.Blue, px, py1, px, py2);
}
Result:
Of course finding the right values for Interval, IntervalOffset, Minimum and Maximum is all up to you..
Update: If you want to have a full set of shortened x-axis gridlines you could do a lot of math or use a graphics transform. As usual the latter is so much easier..:
Graphics g = e.ChartGraphics.Graphics;
int pyc = (int)ay.ValueToPixelPosition(ay.Minimum); // y-center
for (int i = 0; i < 360 / ax.Interval; i++)
{
g.TranslateTransform(px, pyc);
g.RotateTransform((float)(i * ax.Interval));
g.TranslateTransform(-px, -pyc);
g.DrawLine(Pens.colorOfYourChoice, px, py1, px, py2);
g.ResetTransform();
}
After setting ax.Interval = 30; we get this result:
I created a Line Chart control in Windows Forms.
I divided the ChartArea, AxisX into four intervals but I want to apply back color (unique color) to each interval.
Can someone help me on this?
You could paint those areas, but this would always show above all chart elements including grid and data points.
So, as NLindborn suggests, the best way are StripLines.
They are under all elements and will scale nicely.
Note that their properties are in data values, so you need to know the values, or rather the x-axis range, in your chart.
Here is complete code example using StripLines:
// set up the chart:
ChartArea ca = chart.ChartAreas[0];
chart.Series.Clear();
for (int i = 0; i < 5; i++)
{
Series s = chart.Series.Add("Series" + (i+1));
s.ChartType = SeriesChartType.Line;
s.BorderWidth = 2;
}
// add a few test data
for (int i = 0; i <= 360; i++)
{
chart.Series[0].Points.AddXY(i, Math.Sin(i * Math.PI / 180f));
chart.Series[1].Points.AddXY(i, Math.Cos(i * Math.PI / 180f));
chart.Series[2].Points.AddXY(i, Math.Sin(i * Math.PI / 90f));
chart.Series[3].Points.AddXY(i, Math.Cos(i * Math.PI / 90f));
chart.Series[4].Points.AddXY(i, Math.Sin(i * Math.PI / 30f));
}
// set up the chart area:
Axis ax = ca.AxisX;
ax.Minimum = 0;
ax.Maximum = 360;
ax.Interval = 30;
// a few semi-transparent colors
List<Color> colors = new List<Color>() { Color.FromArgb(64, Color.LightBlue),
Color.FromArgb(64, Color.LightSalmon), Color.FromArgb(64, Color.LightSeaGreen),
Color.FromArgb(64, Color.LightGoldenrodYellow)};
Now we are ready to create the StripLines:
// this is the width of the chart in values:
double hrange = ax.Maximum - ax.Minimum;
// now we create and add four striplines:
for (int i = 0; i < 4; i++)
{
StripLine sl = new StripLine();
sl.Interval = hrange; // no less than the range, so it won't repeat
sl.StripWidth = hrange / 4f; // width
sl.IntervalOffset = sl.StripWidth * i; // x-position
sl.BackColor = colors[i];
ax.StripLines.Add(sl);
}
Note that you will need to adapt the stripline data whenever you change the axis range!
Also note the StripLine use axis values.
Update:
One common issue is to move the striplines when zooming. Without a little help they will stick to the original positions. Codeing the AxisViewChanged will help, maybe like so:
For each of your striplines calculate an IntervalOffset; in the simplest case of the 1st one this should work:
chart1.ChartAreas[0].AxisX.StripLines[0].IntervalOffset =
chart1.Series[0].Points[0].XValue - e.NewPosition;
For the others add the correct multiple of the width as above!
AxisX into four intervals but I want to apply back color (unique color)
These intervals are created with colored StripLine(s). Either via code:
var stripLine = new System.Windows.Forms.DataVisualization.Charting.StripLine()
{
BackColor = Color.Blue,
IntervalOffset = 4, // This is where the stripline starts
StripWidth = 2 // And this is how long the interval is
};
chart1.ChartAreas[0].AxisX.StripLines.Add(stripLine);
You need to add data points for the interval to show.
Or, StripLines can also be added from VS design mode from (Properties) -> ChartAreas -> Select a ChartArea -> Axes -> Select the Axis you want it to show on -> StripLines, then Add StripLine. You have to set a BackColor, IntervalOffset and StripWidth for it to show. If you set StripLine.Interval it will repeat by that interval.
The results I want are one main chart and several sub charts which are synchronized with the main chart. (Sub charts must share the same x-axis with the main chart.)
At first, I tried it using multiple chartAreas. I can sync sub chartAreas with the main chartArea. (Using this: How to align two (or more) charts in size, scroll and grid) This one is what I want exactly. But I can't only vertical scroll the sub-chartAreas. If I scroll, all the charts are scrolled. I want to vertical scroll only sub-chartAreas. (to show the main chartArea at top anytime even when the scroll bar is down)
So, I reverse a decision to use multiple charts (not chart Areas). I can place them into TableLayoutPanel. One chart per row. Then, I cannot sync their x-axes...
Is there any way to sync an x-axis with multiple charts?
Or to scroll only sub-chartAreas using multiple chartAreas?
By 'scroll' I mean Vertical scroll. See here:
From the image and the comments I gather that you actually want to scroll down a chart with several ChartArea but keep one fixed at the top.
((If that is so you should correct the question title!))
By default a Chart can only scroll the data within a zoomed ChartArea, not several ChartAreas within the Chart.
Here is an example of faking both ChartArea scrolling and freeezing the top ChartArea.
Here are the necessary steps:
We need a VerticalScrollbar control and anchor it to the right of the Chart.
We need to set its Minumum and Maximum fields to suitable values; I left the minimum at 0 and calculate the max with a few params..
Then we can code its Scroll event..:
private void vScrollBar1_Scroll(object sender, ScrollEventArgs e)
{
float h = (100 - yOffTop - yOffBottom) / visibleCount;
ChartArea main = chart1.ChartAreas[0];
for (int i = 1; i < chart1.ChartAreas.Count; i++)
{
ChartArea ca = chart1.ChartAreas[i];
ca.Position = new ElementPosition(0, h * i - vScrollBar1.Value + mainExtra, 80, h);
ca.Visible = ca.Position.Y >= main.Position.Bottom ;
}
}
visibleCount controls how many ChartAreas are visible. In this example I have one for the year, fixed at the top and 12 more for the months..:
For this to work you need to set up the chart so that the chartareas are intialized in a suitable way.
I used this piece of code, do use your own code for stuff like right space (I left 20% for the legend) etc..:
int visibleCount = 5;
float yOffTop = 0; // no extra top space for a chart title
float yOffBottom = 0; // no extra bottom space either
float mainExtra = 6; // a few extra% for the main CA's axis labels
private void Init_button_Click(object sender, EventArgs e)
{
chart1.Series.Clear();
chart1.ChartAreas.Clear();
chart1.BackColor = Color.Snow;
float h = (100 - yOffTop - yOffBottom) / visibleCount;
for (int i = 0; i < 13; i++)
{
float yOff = i != 0 ? mainExtra : 0;
float yExtra = i == 0 ? mainExtra : 0;
ChartArea ca = chart1.ChartAreas.Add("ca" + i);
ca.Position = new ElementPosition(0, h * i + yOff , 80, h + yExtra);
ca.BackColor = Color.FromArgb(i * 20, 255 - i * 3, 255);
ca.AxisX.IntervalOffset = 1;
Series s = chart1.Series.Add("s" + i);
s.ChartArea = ca.Name;
for (int j = 1; j < 30; j++)
{
s.Points.AddXY(j, rnd.Next(100) - rnd.Next(20));
}
chart1.ChartAreas[0].BackColor = Color.Silver;
ca.AxisY.Title = i == 0 ? "Year" :
DateTimeFormatInfo.CurrentInfo.GetMonthName(i);
ca.AxisX.Enabled = (i == 0) ? AxisEnabled.True : AxisEnabled.False;
}
vScrollBar1.Minimum = 0;// (int)( h);
vScrollBar1.Maximum = (int)((chart1.ChartAreas.Count - visibleCount + 0.5f) * h
+ mainExtra );
}
I draw extra rectangles around each CA, just for testing; do ignore them!
Note: Working with those percentages is always a little tricky and may take some tweaking!
While using a self-written graphing control I noticed that the painting of the graph was much slower while displaying noisy data than when it displayed clean data.
I dug further into and narrowed the problem down to its bare minimum difference: Drawing the same amount of lines with varying Y values versus drawing lines with the same Y value.
So for example I put together the following tests. I generate lists of points, one with random Y values, one with the same Y, and one with a Zig-Zag Y pattern.
private List<PointF> GenerateRandom(int n, int width, int height)
{
//Generate random pattern
Random rnd = new Random();
float stepwidth = Convert.ToSingle(width / n);
float mid = Convert.ToSingle(height / 2);
float lastx = 0;
float lasty = mid;
List<PointF> res = new List<PointF>();
res.Add(new PointF(lastx, lasty));
for (int i = 1; i <= n; i++)
{
var x = stepwidth * i;
var y = Convert.ToSingle(height * rnd.NextDouble());
res.Add(new PointF(x, y));
}
return res;
}
private List<PointF> GenerateUnity(int n, int width, int height)
{
//Generate points along a simple line
float stepwidth = Convert.ToSingle(width / n);
float mid = Convert.ToSingle(height / 2);
float lastx = 0;
float lasty = mid;
List<PointF> res = new List<PointF>();
res.Add(new PointF(lastx, lasty));
for (int i = 1; i <= n; i++)
{
var x = stepwidth * i;
var y = mid;
res.Add(new PointF(x, y));
}
return res;
}
private List<PointF> GenerateZigZag(int n, int width, int height)
{
//Generate an Up/Down List
float stepwidth = Convert.ToSingle(width / n);
float mid = Convert.ToSingle(height / 2);
float lastx = 0;
float lasty = mid;
List<PointF> res = new List<PointF>();
res.Add(new PointF(lastx, lasty));
var state = false;
for (int i = 1; i <= n; i++)
{
var x = stepwidth * i;
var y = mid - (state ? 50 : -50);
res.Add(new PointF(x, y));
state = !state;
}
return res;
}
I now draw each list of points a few times and compare how long it takes:
private void DoTheTest()
{
Bitmap bmp = new Bitmap(970, 512);
var random = GenerateRandom(2500, bmp.Width, bmp.Height).ToArray();
var unity = GenerateUnity(2500, bmp.Width, bmp.Height).ToArray();
var ZigZag = GenerateZigZag(2500, bmp.Width, bmp.Height).ToArray();
using (Graphics g = Graphics.FromImage(bmp))
{
var tUnity = BenchmarkDraw(g, 200, unity);
var tRandom = BenchmarkDraw(g, 200, random);
var tZigZag = BenchmarkDraw(g, 200, ZigZag);
MessageBox.Show(tUnity.ToString() + "\r\n" + tRandom.ToString() + "\r\n" + tZigZag.ToString());
}
}
private double BenchmarkDraw(Graphics g, int n, PointF[] Points)
{
var Times = new List<double>();
for (int i = 1; i <= n; i++)
{
g.Clear(Color.White);
System.DateTime d3 = DateTime.Now;
DrawLines(g, Points);
System.DateTime d4 = DateTime.Now;
Times.Add((d4 - d3).TotalMilliseconds);
}
return Times.Average();
}
private void DrawLines(Graphics g, PointF[] Points)
{
g.DrawLines(Pens.Black, Points);
}
I come up with the following durations per draw:
Straight Line: 0.095 ms
Zig-Zag Pattern: 3.24 ms
Random Pattern: 5.47 ms
So it seems to get progressively worse, the more change there is in the lines to be drawn, and that is also a real world effect I encountered in the control painting I mentioned in the beginning.
My questions are thus the following:
Why does it make a such a brutal difference, which lines are to be drawn?
How can I improve the drawing speed for the noisy data?
Three reasons come to mind:
Line Length : Depending on the actual numbers sloped lines may be longer by just a few pixels or a lot or even by some substantial factor. Looking at your code I suspect the latter..
Algorithm : Drawing sloped lines does take some algorithm to find the next pixels. Even fast drawing routines need to do some computations as opposed to vertical or horizontal lines, which run straight through the pixel arrays.
Anti-Aliasing : Unless you turn off anti-aliasing completely (with all the ugly consequences) the number of pixels to paint will also be around 2-3 times more as all those anti-aliasing pixels above and below the center lines must also be calculated and drawn. Not to forget calculating their colors!
The remedy for the latter part is obviously to turn off anti-aliasing, but the other problems are simply the way things are. So best don't worry and be happy about the speedy straight lines :-)
If you really have a lot of lines or your lines could be very long (a few time the size of the screen), or if you have a lot of almost 0 pixel line, you have to wrote code to reduce useless drawing of lines.
Well, here are some ideas:
If you write many lines at the same x, then you could replace those by a single line between min and max y at that x.
If your line goes way beyond the screen boundary, you should clip them.
If a line is completly outside of the visible area, you should skip it.
If a line have a 0 length, you should not write it.
If a line has a single pixel length, you should write only that pixel.
Obviously, the benefit depends a lot on how many lines you draw... And also the alternative might not give the exact same result...
In practice, it you draw a chart on a screen, then if you display only useful information, it should be pretty fast on modern hardware.
Well if you use style or colors, it might not be as trivial to optimize the displaying of the data.
Alternatively, they are some charting component that are optimized for display large data... The good one are generally expensive but it might still worth it. Often trials are available so you can get a good idea on how much you might increase the performance and then decide what to do.
I'm working with the System.Windows.Forms.DataVisualization.Charting library of C# in Visual Studio.
Creating the graphs themselves is no problem, however, since I'm using SeriesChartType.StackedArea100 for my serieses (which always fills the vertical graph space 100%), the grid (X & Y) is completely covered by the graphs.
However, I want the X-grid to be above the graphs, so it's easier to see which point belongs to what.
Am I missing something obvious here?
Gridlines are always drawn under the DataPoints.
One option is to make the Colors of the DataPoints semi-transparent.
Here is an example:
chart1.ApplyPaletteColors(); // necessary to access the original colors
if (checkBox1.Checked)
{
foreach (Series s in chart1.Series) s.Color = Color.FromArgb(192, s.Color);
}
You can raise alpha to 224 and still see the lines.
Or you could owner-draw GridLines in one of the xxxPaint events; but that of course is a little more complicated. OK, a lot more..
The drawing itself is regular GDI+ drawing with DrawLine calls in two loops.
But to get the loops and the coordinates right you need to :
Make sure you know/control the Minimum, Maximum & Interval for the axes. If they are not set but still on their auto-values you need to find a way to get at them.
know the Rectangle of the InnerPlotPosition in pixels(!). See here for two functions that will help you !
Here is an example:
private void chart1_PostPaint(object sender, ChartPaintEventArgs e)
{
if (checkBox1.Checked) return;
ChartArea ca = chart1.ChartAreas[0];
RectangleF ipar = InnerPlotPositionClientRectangle(chart1, ca);
Axis ax = ca.AxisX;
Axis ay = ca.AxisY;
Color gc = ax.MajorGrid.LineColor;
Pen pen = new Pen(gc);
double ix = ax.Interval == 0 ? 1 : ax.Interval; // best make sure to set..
double iy = ay.Interval == 0 ? 50 : ay.Interval; // ..the intervals!
for (double vx = ax.Minimum; vx <= ax.Maximum; vx+= ix)
{
int x = (int)ax.ValueToPixelPosition(vx) + 1;
e.ChartGraphics.Graphics.DrawLine(pen, x, ipar.Top, x, ipar.Bottom);
}
for (double vy = ay.Minimum; vy <= ay.Maximum; vy += iy)
{
int y = (int)ay.ValueToPixelPosition(vy) + 1;
e.ChartGraphics.Graphics.DrawLine(pen, ipar.Left, y, ipar.Right, y);
}
pen.Dispose();
}
You should disable the GridLines and maybe even make the the Axes transparent:
chart1.ChartAreas[0].AxisX.MajorGrid.Enabled = false;
chart1.ChartAreas[0].AxisY.MajorGrid.Enabled = false;
chart1.ChartAreas[0].AxisX.LineColor = Color.Transparent;
chart1.ChartAreas[0].AxisY.LineColor = Color.Transparent;