I have Visual Studio 2019. The project is a .Net Windows Form on C# on .Net Framework 4.8.
I have a Datagridview which shows some tables data from different databases (MS SQL and Postgresql).
I merge that databases, and the result is too long, so we can't fit it on a screen; but we have to see all the data available on that screen, which are more than 40 columns. Reduce the font size is not plausible.
So, the solution proposed was to merge some values on the same column in this way (See this example):
The actual data view:
The way we need to view it:
If you have any ideas or you know an alternative to Datagridview which allows that, please share them.
Thanks in advance.
You can customize your datagridview's row and column to get the Multi-line columns on a datagridview.
I assume that the datatable is the table from database.
using System;
using System.Data;
using System.Linq;
using System.Windows.Forms;
private void Form1_Load(object sender, EventArgs e)
{
DataTable table = new DataTable();
table.Columns.Add("Name");
table.Columns.Add("Field1");
table.Columns.Add("Field2");
table.Columns.Add("Field3");
table.Columns.Add("Field4");
table.Columns.Add("Field5");
table.Columns.Add("Field6");
table.Columns.Add("Field7");
table.Rows.Add("test1", 1, 2, 3, 4, 5, 6, 7);
table.Rows.Add("test2", 1, 2, 3, 4, 5, 6, 7);
table.Rows.Add("test3", 1, 2, 3, 4, 5, 6, 7);
dataGridView1.ColumnHeadersVisible = false;
for (int i = 0; i < table.Columns.Count/2; i++)
{
dataGridView1.Columns.Add("","");
}
string[] columnNames = table.Columns.Cast<DataColumn>()
.Select(x => x.ColumnName)
.ToArray();
int count = table.Columns.Count/2;
var col1 = columnNames.Take(count).ToArray();
var col2= columnNames.Skip(count).Take(count).ToArray();
dataGridView1.Rows.Add(col1);
dataGridView1.Rows.Add(col2);
object[] arr;
for (int i = 0; i < table.Rows.Count; i++)
{
arr = table.Rows[i].ItemArray;
var row1=arr.Take(count).ToArray();
var row2 = arr.Skip(count).Take(count).ToArray();
dataGridView1.Rows.Add(row1);
dataGridView1.Rows.Add(row2);
}
}
Result:
After reviewing your question, I have to say that IMHO your solution is not giving much thought to the end user or if the code has to grab one of the values. Stacking fields into a single column “creates” two issues IMHO…One, as mentioned, is that the user is going to have to do extra work and check the order of the headers to distinguish which field value is which… a subtle yet (annoying) non intuitive extra step. Two, if the user is allowed to change fields or the code needs to grad a field, then, there is going to be extra work needed to differentiated which field goes with which value. Extra work for the user and extra work for the coder doesn’t sound like a good start.
Sorry about my rant. Fortunately, if you wanted to take a table as shown in the question, and turn it into a table as you describe, then the code below should do this. It basically creates “two” (2) field columns. Such that each column contains two fields. The code is hacky yet it is not too complicated I hope. I made numerous comments in the code. Some notes would be that, since we are adding two fields for each column and (as far as I know) a DataGridView won’t allow double column headers, the code does NOT use the column headers row and instead uses the first two rows of the grid for the two column headers. This will allow you to format the two rows to look like headers and/or color code if needed.
Lastly, a better solution IMHO. As previously mentioned a pivot will work, however, there are a couple of issues given how the data is stored in the original table. In a basic pivot where we switch rows and columns, the posted example would have three (3) columns… “Jim”, “Hugh” and “Terrance”. Then the number of rows would be (one (1) + however many fields/field columns). The extra “one” is the field “LastName.” Given this, it may look something like…
Jim Hugh Terrance
LastName Carey Jackman Hill
Field1 1 a N/A
Field2 2 b N/A
……..
It would appear obvious that the “LastName” should go with the column header. Therefore, the transpose/pivot may look like…
Jim Carey Hugh Jackman Terrance Hill
Field1 1 a N/A
Field2 2 b N/A
……..
IMHO, this will be more intuitive for the user to identify fields and there should not be any extra coding if we need to reference a specific value. The picture below shows a complete example from the code below. Drop three (3) DataGridViews onto a form and paste my code. The top-left grid is the original data. The bottom-left grid is the transpose as per your requirements and finally, the grid on the right is the what I feel would work best considering your dilemma.
A note on the last grid on the right… Initially, like the column header rows in your solution, I had the fields as a column in the grid. It will not be difficult to change the code if you want this. However, the code currently adds the field names as “row headers” in the grid. Since the DataTable does not really have row headers, this addition had to be made “after” the data source was set and can be seen in the forms Load event. Again, it will not be difficult to move the field to an added column in the DataTable.
To make this example complete, below is code to create some test data. The incoming parameter totalCols will make totalCols columns in the DataTable for the "Field" values.
private DataTable GetDataFromDB(int totalCols) {
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("LastName", typeof(string));
for (int i = 1; i <= totalCols; i++) {
dt.Columns.Add("Field" + i, typeof(string));
}
DataRow curRow;
string name;
string lName;
for (int i = 1; i < 4; i++) {
switch (i) {
case 1:
name = "Jim";
lName = "Carrey";
break;
case 2:
name = "Hugh";
lName = "Jackman";
break;
default:
name = "Terence";
lName = "Hill";
break;
}
curRow = dt.NewRow();
curRow["Name"] = name;
curRow["LastName"] = lName;
if (i < 3) {
for (int j = 2; j < dt.Columns.Count; j++) {
curRow[j] = "N" + i + "F" + (j - 1);
}
}
dt.Rows.Add(curRow);
}
return dt;
}
We will use three (3) global DataTables, one for each grid.
DataTable originalDT;
DataTable pivotDT1;
DataTable pivotDT2;
The load event that sets each grid to the proper DataTable and some specific formatting for each grid.
private void Form1_Load(object sender, EventArgs e) {
originalDT = GetDataFromDB(45);
dataGridView1.DataSource = originalDT;
// pivot 1 - bottom left grid
pivotDT1 = PivotTable(originalDT);
dataGridView2.DataSource = pivotDT1;
dataGridView2.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
dataGridView2.Rows[0].DefaultCellStyle.BackColor = Color.Blue;
dataGridView2.Rows[1].DefaultCellStyle.BackColor = Color.Blue;
dataGridView2.Rows[0].DefaultCellStyle.ForeColor = Color.White;
dataGridView2.Rows[1].DefaultCellStyle.ForeColor = Color.White;
dataGridView2.Columns[0].Frozen = true;
// pivot 2 - right grid
pivotDT2 = PivotTable2(originalDT);
dataGridView3.DataSource = pivotDT2;
int dgvRow = 0;
// add column headers as row headers in the grid
for (int i = 2; i < originalDT.Columns.Count; i++) {
dataGridView3.Rows[dgvRow++].HeaderCell.Value = originalDT.Columns[i].ColumnName;
}
dataGridView3.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.AutoSizeToAllHeaders;
}
Finally, the two pivot/transform methods…
Using your solution and shown in the bottom-left grid…
private DataTable PivotTable(DataTable originalDT) {
DataTable pivotDT = new DataTable();
// the number of columns will be half the original number of columns
int halfCols = Math.DivRem(originalDT.Columns.Count, 2, out int rem);
// if there is a remainder then there is an odd number of columns and we need to add 1 col
if (rem > 0) {
halfCols++;
}
// add the columns to the pivot table
for (int i = 0; i < halfCols; i++) {
pivotDT.Columns.Add();
}
// the number of rows will be the number of original rows times 2
// PLUS 2 additional rows for the headers
for (int i = 0; i < (originalDT.Rows.Count * 2) + 2; i++) {
pivotDT.Rows.Add();
}
// Add the two header rows from the column names
int originalCol = 0;
for (int i = 0; i < halfCols; i++) {
pivotDT.Rows[0][i] = originalDT.Columns[originalCol++].ColumnName;
// if the original table had an odd number of columns
// then the last column only had one field
// - there would never be a column without at least one field
if (originalCol < originalDT.Columns.Count) {
pivotDT.Rows[1][i] = originalDT.Columns[originalCol++].ColumnName;
}
}
// finally add the rows from the original table.
int pivotRow = 2;
int pivotCol = 0;
int curPivotRow;
int curPivotCol;
string value;
for (int originalRow = 0; originalRow < originalDT.Rows.Count; originalRow++) {
curPivotRow = pivotRow;
curPivotCol = pivotCol;
for (originalCol = 0; originalCol < originalDT.Columns.Count; originalCol++) {
value = originalDT.Rows[originalRow][originalCol].ToString();
if (string.IsNullOrEmpty(value)) {
value = "N/A";
}
pivotDT.Rows[curPivotRow][curPivotCol] = value;
// if this is the first item then simply bump the pivot row
if (curPivotRow < pivotRow + 1) {
curPivotRow++;
}
else { // this is the second item -
// we want the curpivot row to start back at the starting pivotRow
// then move over a column for the next two columns in the original table
curPivotRow = pivotRow;
curPivotCol++;
}
}
// new row in the original data start back at column 0 in the pivot table
// and bump the row index by two since we added two rows
pivotRow += 2;
pivotCol = 0;
}
return pivotDT;
}
And my solution shown in the grid on the right.
private DataTable PivotTable2(DataTable originalDT) {
DataTable pivotDT = new DataTable();
for (int i = 0; i < originalDT.Rows.Count; i++) {
pivotDT.Columns.Add();
}
for (int i = 0; i < originalDT.Columns.Count - 2; i++) {
pivotDT.Rows.Add();
}
int pivotCol = 0;
foreach (DataRow row in originalDT.Rows) {
pivotDT.Columns[pivotCol++].ColumnName = row[0].ToString() + " " + row[1].ToString();
}
int pivotRow = 0;
pivotCol = 0;
string value;
for (int i = 0; i < originalDT.Rows.Count; i++) {
for (int j = 2; j < originalDT.Columns.Count; j++) {
value = originalDT.Rows[i][j].ToString();
if (string.IsNullOrEmpty(value)) {
value = "N/A";
}
pivotDT.Rows[pivotRow++][pivotCol] = value;
}
pivotCol++;
pivotRow = 0;
}
return pivotDT;
}
Finally, I am not that proficient using SQL, however, I am betting it is possible to create an SQL procedure that will produce my solution directly from the data base.
I'm trying to set a percent format when I create a cell, this is my code.
new Data.CellData
{
UserEnteredValue = new Data.ExtendedValue
{
FormulaValue = "=(B2/C2)-1"
},
EffectiveFormat = new Data.CellFormat
{
NumberFormat = new Data.NumberFormat
{
Pattern = "00.0%",
Type = "NUMBER"
}
}
}
Current value shown is "-0.2488570834" and I need this "-24.88%". What I'm missing ?
Thanks in advance.
Try following the Number format examples provided in the document:
I've also found a sample code to test this (converting format to your sample):
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getActiveRange();
var rows = range.getNumRows();
var cols = range.getNumColumns();
for(var row = 1; row <= rows; row++) {
for(var col = 1; col <= cols; col++) {
var cell = range.getCell(row, col);
var value = cell.getValue();
if(typeof(value) == 'number') {
cell.setNumberFormat("##.#%");
}
}
}
Result:
The code sample is for Apps script but the implementation to C# would be the same. Change Pattern = "00.0%", to Pattern = "##.#%",.
Hope this helps.
I'm working on a chart in asp.net that is dinamically created from a DataTable. Basically I've got a DataTable like this:
So, what I want in my chart is a column for each differente location, and for each diferent Type I want to have a "portion" of the column, meaning I would like to have a column per location divided per type.
I have managed to do something like this, but not quite what I would like:
Now I'll try to explain my code.
First, i add the different locations in my datatable to a list of strings
string loc = "";
for (int i = 0; i < dtTipos.Rows.Count; i++)
{
if (loc != dtTipos.Rows[i]["Location"].ToString())
listaLocais.Add(dtTipos.Rows[i]["Location"].ToString());
loc = dtTipos.Rows[i]["Location"].ToString();
}
After, for each different location, I select the rows to the corresponding location and create a series for each one of them. I proceed to create a point with y=0 and one with y=total
for (int i = 0; i < listaLocais.Count; i++)
{
DataRow[] lRow = dtTipos.Select("Location = '" + listaLocais[i] + "'");
foreach (DataRow dr in lRow)
{
string serie = dr["Location"].ToString().Substring(0, 2) + "_" + dr["Type"].ToString() + " - " + dr["Total"].ToString();
Series s = new Series();
s.Name = serie;
s.Label = dr["Location"].ToString();
s.ChartType = SeriesChartType.StackedColumn;
chartTipo1.Series.Add(s);
s.Points.AddXY(i + 1, 0);
s.Points[0].Label = " ";
s.Points.AddXY(i + 1,Convert.ToInt32(dr["Total"].ToString()));
s.Points[1].Label = " ";// dtTipos.Rows[i]["Total"].ToString();
}
}
The thing is: I think that because I create a serie for each row, everytime I use a diferent column, the y axis starts from where I left it.
Is there any way so I can get this to work?
I'll appreciate any help.
You're adding your series and data points in the same loop. First add your series, then your data points:
public partial class WebForm1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
DataTable dt = new MyDataTable();
//first add your series
foreach (DataRow row in dt.DefaultView.ToTable(true, new string[] { "Type" }).Rows)
{
Series series = new Series();
series.Name = (string)row["Type"];
series.ChartType = SeriesChartType.StackedColumn;
Chart1.Series.Add(series);
}
// then add your points;
foreach (DataRow row in dt.Rows)
Chart1.Series[(string)row["Type"]].Points.AddXY(row["Location"], new object[] { row["Total"] });
}
}
I'm creating a dotnet high chart by using the code:
object[,] data1 = new object[Dt.Rows.Count, 2];
for (int i = 0; i < Dt.Rows.Count; i++)
{
data1[i, 0] = Dt.Rows[i]["IncDate"];
data1[i, 1] = Dt.Rows[i]["IncCost"];
}
object[,] data2 = new object[Dt2.Rows.Count, 2];
for (int i = 0; i < Dt2.Rows.Count; i++)
{
data2[i, 0] = Dt2.Rows[i]["ExpDate"];
data2[i, 1] = Dt2.Rows[i]["ExpCost"];
}
Highcharts chart = new Highcharts("graph")
.SetTitle(new Title { Text = "Money Analysis" })
.SetXAxis(new XAxis { Type = AxisTypes.Datetime })
.SetYAxis(new YAxis { Title = new YAxisTitle { Text = "Amount (£)" } })
.SetSeries(new[]
{
new Series { Name = "Incomings", Color = Color.LimeGreen, Data = new Data(data1) },
new Series { Name = "Expenditures", Color = Color.Red, Data = new Data(data2) }
})
which outputs:
I was wondering how I'm able to make the red line continue along the current cost until the end of the graph (the last date for the incoming.) And also do the same for the green line if the expenditures have a later date than the last incomings date. Hopefully people know what I mean. Thanks in advance
So, no - the chart won't do that for you automatically, but we can do it ourselves easy enough.
1) retrieve the final y value of your data
2) retrieve the final x value that you want it to extend until
3) add a new point to the data with those x and y values.
Example function:
function extendData(series, axis) {
var ext = axis.getExtremes();
var x = ext.dataMax;
var y = series.data[series.data.length -1].y;
series.addPoint({'x':x, 'y':y, marker: { enabled: true }});
}
Working example:
http://jsfiddle.net/jlbriggs/omtugbvo/
And just to show an example using any number of series:
http://jsfiddle.net/jlbriggs/omtugbvo/3/
Example image:
I am trying to create a line chart that shows test results over a period of time (interval of weeks). It's the first time I've used the chart control and I seem to keep displaying a grey square if I add points in from a loop:
Like this http://imageshack.us/a/img69/4718/69sq.png
I just can't see where I'm going wrong in my code - if i add some generic points in by hand then it will display properly.
Here is the code I'm using:
chtBreakdown.ChartAreas[0].AxisY.Minimum = 0;
chtBreakdown.ChartAreas[0].AxisY.Maximum = 100;
chtBreakdown.ChartAreas[0].AxisY.Interval = 10;
chtBreakdown.ChartAreas[0].AxisX.IntervalType = DateTimeIntervalType.Weeks;
chtBreakdown.ChartAreas[0].AxisX.Interval = 1;
dtiStart.Value = DateTime.Now.AddMonths(-3);
dtiEnd.Value = DateTime.Now;
chtBreakdown.Series.Clear();
DateTimeOffset minimum = dtiStart.Value;
DateTimeOffset maximum = dtiEnd.Value;
chtBreakdown.ChartAreas[0].AxisX.Minimum = minimum.DateTime.ToOADate();
chtBreakdown.ChartAreas[0].AxisX.Maximum = maximum.DateTime.ToOADate();
foreach (User u in allUsers)
{
List<Training> userTraining = u.TrainingList.Where(t => t.StartTime >= minimum && t.StartTime <= maximum).OrderBy(t => t.EndTime).ToList();
if (userTraining.Count != 0)
{
Series series = new Series(u.DisplayName);
series.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
series.BorderWidth = 2;
series.XValueType = ChartValueType.DateTime;
foreach (Training t in userTraining) series.Points.AddXY(t.StartTime.DateTime, t.PassPercentage);
chtBreakdown.Series.Add(series);
}
}
Can anybody show me where I'm going wrong?
Your Series instances are probably not associated to any ChartArea by default. Try to add this:
series.ChartArea = chtBreakdown.ChartAreas[0].Name;