Pareto Chart C#






4.89/5 (28投票s)
使用 C# 的 Pareto Chart 控件
- 下载 SHANUParetoChart_FRAMEWORK2 - 178.4 KB
- 下载 SHANUParetoChart - 277.3 KB
- 下载 SHANUParetoChart - 340.9 KB
如果您喜欢我的文章和 Pareto Chart 控件,请留下评论,别忘了为我的文章投票。希望这个控件能帮到您。
引言
本文的主要目的是创建一个简单的 Pareto Chart 控件。市面上有一些第三方 Pareto Chart,也可以使用 MS Chart 控件制作 Pareto Chart。我的目标是创建一个简单的 Pareto Chart 用户控件。因此,是的,我使用 C#.NET 创建了一个 Pareto Chart 用户控件。首先,我们从 Pareto Chart 是什么开始。在我的 Pareto Chart 控件中,用户可以添加 X 轴绘图数据,最少 3 个,最多 10 个。您可以根据自己的需求修改我的源代码。通过少量源代码修改,您还可以将其更改为折线图、直方图和条形图。
Pareto Chart (帕累托图)
Pareto Chart 是条形图和折线图的组合。在 Pareto Chart 的 X 轴上,我们将绘制数据名称。在左侧 Y 轴上,我们将绘制数据频率,在右侧 Y 轴上,我们将绘制数据累积频率百分比。Pareto Chart 用于图形化地总结和显示不同数据组之间的相对重要性。
Pareto Chart 的规则是将最高到最低频率的数据作为条形图显示。将累积频率百分比数据显示为折线图。
这里,我们可以看到一个示例数据
S.No Name Freq Cum Freq Cum Per %
1 Pareto1 53 53 19.27272727
2 pareto2 45 98 35.63636364
3 pareto3 43 141 51.27272727
4 pareto4 37 178 64.72727273
5 pareto5 35 213 77.45454545
6 pareto6 27 240 87.27272727
7 pareto7 23 263 95.63636364
8 pareto8 12 275 100
Sum 275
上面的示例数据已在我的程序中使用以显示 Pareto Chart 的结果。从上面的数据中,我们可以看到。
Fre (频率)
频率是每个样本数据的总计数。例如,样本 pareto1
有 53 个数据计数,pareto2
有 45 个,依此类推。
频率 = 每个数据的总计数
Cum Freq (累积频率)
累积频率是前一个频率加上当前频率。例如,第一个频率是 53,所以累积频率将是 53。下一个频率是 45,所以累积频率将是 45+53=98,依此类推。
累积频率 = 前一个频率 + 当前频率 (对于第一个数据,前一个频率将为零)。
Cum per % (累积频率百分比)
累积频率百分比将通过累积频率除以频率总和再乘以 100 来计算。例如,频率总和是 275
。第一个累积频率是 53,所以 53/275*100=19.27。
累积频率百分比 = 频率总和 / 当前累积频率 *100
从下图中,我们可以看到包含上述示例数据和结果的示例 Excel 文件,以及我的 Pareto Control Chart 和结果。
现在我们开始编写代码
我创建了一个 Pareto Chart 作为用户控件,以便可以在所有项目中轻松使用。
在本文中,我附带了一个名为 SHANUParetoChart.zip 的 zip 文件,其中包含
- "ShanuParetoChart_CTL" 文件夹 (此文件夹包含 Pareto Chart 用户控件的源代码)。
注意:我将数据作为 DataTable 传递给用户控件。用户可以遵循相同的方法,或根据您的要求更改我的代码。
DataTable 的格式是第一列为序号,第二列为数据名称,第三列为频率。对于我的示例,我将为每个样本传递总数据计数作为频率。我创建的 DataTable 如下所示
DataTable dt = new DataTable(); dt.Columns.Add("No"); dt.Columns.Add("Names"); dt.Columns.Add("Frequency"); dt.Columns["Frequency"].DataType = Type.GetType("System.Int32");
- "SHANUParetoChart_DEMO" 文件夹 (此文件夹包含演示程序,其中包含带有使用
Timer
控件的随机数据样本的paretoChart
用户控件)。
Using the Code
- 首先,我们将从用户控件开始。要创建用户控件,
- 创建一个新的 Windows 控件库项目。
- 设置项目名称,然后点击 确定 (此处,我的用户控件名称为
ShanuParetoChart_CTL
)。 - 添加所有需要的控件。
- 在代码隐藏中,声明所有
public
变量和Public
属性变量。private System.Windows.Forms.PictureBox PicBox; private System.Windows.Forms.Panel OuterPanel; private System.ComponentModel.Container components = null; private string m_sPicName = ""; ContextMenuStrip docmenu = new System.Windows.Forms.ContextMenuStrip(); ToolStripMenuItem saveimg = new ToolStripMenuItem(); int NoofPlots = 5; public DataTable dt = new DataTable(); Font f12 = new Font("arial", 12, FontStyle.Bold, GraphicsUnit.Pixel); Pen B1pen = new Pen(Color.Black, 1); Pen B2pen = new Pen(Color.Black, 2); int[] intDataValue; int[] intCumulativeValue; Double[] intCumulativeValuePer; Double[] XaxisplotWidth; int First_chartDatarectHeight = 400; int First_chartDatarectWidth = 600; Font f10 = new Font("arial", 10, FontStyle.Bold, GraphicsUnit.Pixel); LinearGradientBrush a2 = new LinearGradientBrush(new RectangleF(0, 0, 100, 19), Color.DarkGreen, Color.Green, LinearGradientMode.Horizontal); LinearGradientBrush a3 = new LinearGradientBrush(new RectangleF(0, 0, 100, 19), Color.DarkRed, Color.Red, LinearGradientMode.Horizontal); LinearGradientBrush a1 = new LinearGradientBrush(new RectangleF(0, 0, 100, 19), Color.Blue, Color.DarkBlue, LinearGradientMode.Horizontal);
- 在
Picturebox Paint
方法中,我调用一个方法来绘制 Pareto Chart。我使用了 .NET GDI+ 来绘制 Pareto Chart 中的条形图和折线图,以及Datatable
结果。在paint
方法中,我检查Datatable
数据,并将频率、累积频率和累积频率百分比存储到数组中。public void PicBox_Paint(object sender, PaintEventArgs e) { int opacity = 58; // 50% opaque (0 = invisible, 255 = fully opaque) e.Graphics.DrawString("SHANU Pareto CHART", new Font("Arial", 28), new SolidBrush(Color.FromArgb(opacity, Color.OrangeRed)), 20, 10); if (dt.Rows.Count <= 2) { return; } int NoofTrials = dt.Rows.Count; int NoofParts = dt.Columns.Count - 1; NoofPlots = NoofTrials; intDataValue = new int[NoofTrials]; intCumulativeValue = new int[NoofTrials]; intCumulativeValuePer = new Double[NoofTrials]; if (dt.Rows.Count <= 2) { return; } int idataval = 0; dt.DefaultView.Sort = "Frequency DESC"; dt = dt.DefaultView.ToTable(); for (int iRow = 0; iRow < dt.Rows.Count; iRow++) { intDataValue[idataval] = System.Convert.ToInt32(dt.Rows[iRow][2].ToString()); idataval = idataval + 1; } int sumofFrequence = intDataValue.Sum(); intCumulativeValue[0] = intDataValue[0]; intCumulativeValuePer[0] = Convert.ToDouble(intCumulativeValue[0]) / Convert.ToDouble(sumofFrequence) * 100; for (int ival = 1; ival < intDataValue.Length; ival++) { intCumulativeValue[ival] = intCumulativeValue[ival - 1] + intDataValue[ival]; intCumulativeValuePer[ival] = Convert.ToDouble (intCumulativeValue[ival]) / Convert.ToDouble(sumofFrequence) * 100; } drawPareto(e.Graphics, sumofFrequence); }
在 "
drawpareto
" 函数中,检查所有数据并绘制 -X 轴、Y 轴(包括频率和累积频率百分比)。读取所有样本频率数据,并使用Draw
和Fill
矩形方法绘制条形图。public void drawPareto(Graphics e, int sumofFrequence) { try { First_chartDatarectHeight = PicBox.Height - 20; First_chartDatarectWidth = PicBox.Width - 20; // 1) For the Chart Data Display --------- e.DrawRectangle(Pens.Black, 10, 10, First_chartDatarectWidth, First_chartDatarectHeight); int historgramHeigt = First_chartDatarectHeight - 30; int historgramWidth = First_chartDatarectWidth - 20; int StartXval = 80; int startyval = 60; // Measurement Horizontal Line e.DrawLine(B1pen, StartXval, historgramHeigt, historgramWidth, historgramHeigt); //Frequency Vertical line e.DrawLine(B1pen, StartXval, startyval, StartXval, historgramHeigt); //Cumulative frequency Percentage Plotting e.DrawLine(B1pen, historgramWidth, startyval, historgramWidth, historgramHeigt); // NoofPlots =7; //Draw Xaxis plots int widthcalculation = (historgramWidth) / NoofPlots + 1; if (widthcalculation <= StartXval) { widthcalculation = StartXval; } int XvalPosition = widthcalculation; widthcalculation = widthcalculation - 18; String[] Xaxisplotdata = new String[NoofPlots]; XaxisplotWidth = new Double[NoofPlots]; RectangleF rectF1 = new RectangleF(30, 10, 100, 122); // End of Xaxis Plot Double yValmaxDataVal = intDataValue.Max(); Double yValminDataVal = intDataValue.Min(); Double yValresultMaxMin = yValmaxDataVal - yValminDataVal; //for plotting the xval data Double yValMeasurementYAxisValue = yValmaxDataVal / NoofPlots; int yValheightcalculation = historgramHeigt / NoofPlots; int yValXvalPosition = yValheightcalculation; Double plotYvals = yValmaxDataVal + 4; Double[] YaxisplotHight = new Double[NoofPlots]; Double[] YaxisplotData = new Double[NoofPlots]; for (int ival = 0; ival < NoofPlots; ival++) { //Draw Xaxis plots String MeasurementXAxisValue = dt.Rows[ival][1].ToString(); Xaxisplotdata[ival] = MeasurementXAxisValue; e.DrawLine(B1pen, XvalPosition, historgramHeigt, XvalPosition, historgramHeigt + 15); rectF1 = new RectangleF(XvalPosition, historgramHeigt + 6, widthcalculation, 34); e.DrawString(MeasurementXAxisValue.ToString(), f10, a2, rectF1); // e.DrawRectangle(Pens.Black, Rectangle.Round(rectF1)); XaxisplotWidth[ival] = XvalPosition; XvalPosition = XvalPosition + widthcalculation; // End of Xaxis Plot // Draw Y axis Plot if (ival == NoofPlots - 1) { e.DrawString("0", f10, a2, StartXval - 12, yValXvalPosition + 4); } else { e.DrawLine(B1pen, StartXval - 10, yValXvalPosition, StartXval, yValXvalPosition); e.DrawString(Math.Round(plotYvals, 0).ToString(), f10, a2, StartXval - 20, yValXvalPosition + 4); } //else //{ // plotYvals = plotYvals - yValMeasurementYAxisValue; //} YaxisplotData[ival] = plotYvals; plotYvals = plotYvals - yValMeasurementYAxisValue; YaxisplotHight[ival] = yValXvalPosition; yValXvalPosition = yValXvalPosition + yValheightcalculation; //End of Yaxis Plot. } int widthcalculation_new = historgramWidth / NoofPlots; int XvalPosition_Start = Convert.ToInt32(XaxisplotWidth[0]); int XvalPosition_new = Convert.ToInt32(XaxisplotWidth[1]) - XvalPosition_Start; widthcalculation = widthcalculation - 18; int Ystartval = 100; int YEndval = 100; //Fill Rectangle LinearGradientBrush a6;// new LinearGradientBrush(new RectangleF (StartXval, Ystartval, widthcalculation_new - StartXval, YEndval), Color.GreenYellow, Color.Green, LinearGradientMode.Vertical); Font f2 = new Font("arial", 12, FontStyle.Bold, GraphicsUnit.Pixel); for (int ival = 0; ival < YaxisplotData.Length - 1; ival++) { for (int yval = 1; yval < YaxisplotData.Length - 1; yval++) { Double finaldisplayvalue = YaxisplotHight[yval - 1] + ((YaxisplotData[yval - 1] - intDataValue[ival]) * 6); if (intDataValue[ival] <= YaxisplotData[yval - 1] && intDataValue[ival] > YaxisplotData[yval]) { Ystartval = Convert.ToInt32(finaldisplayvalue); YEndval = historgramHeigt - Convert.ToInt32(finaldisplayvalue); } else if (intDataValue[ival] <= YaxisplotData[yval - 1] && intDataValue[ival] < YaxisplotData[yval]) { // Double finaldisplayvalue = YaxisplotHight[yval - 1] + // ((YaxisplotData[yval - 1] - intDataValue[ival])); Ystartval = Convert.ToInt32(finaldisplayvalue); YEndval = historgramHeigt - Convert.ToInt32(finaldisplayvalue); } } if (YEndval > 2) { } else { Ystartval = historgramHeigt - 2; YEndval = 2; } a6 = new LinearGradientBrush (new RectangleF(StartXval, Ystartval, XvalPosition_new, YEndval), Color.LightBlue, Color.CornflowerBlue, LinearGradientMode.Vertical); XvalPosition_Start = Convert.ToInt32(XaxisplotWidth[ival]); e.DrawRectangle(B1pen, XvalPosition_Start, Ystartval, XvalPosition_new, YEndval); e.FillRectangle(a6, XvalPosition_Start, Ystartval - 1, XvalPosition_new - 1, YEndval - 1); e.DrawString(intDataValue[ival].ToString(), f2, a2, XvalPosition_Start + 10, Ystartval - 20); XvalPosition_new = Convert.ToInt32(XaxisplotWidth[ival + 1]) - XvalPosition_Start;// XvalPosition_Start+ // XvalPosition_new + widthcalculation_new; if (ival == YaxisplotData.Length - 2) { for (int yval = 1; yval < YaxisplotData.Length - 1; yval++) { if (intDataValue[ival + 1] <= YaxisplotData[yval - 1] && intDataValue[ival] > YaxisplotData[yval]) { Double finaldisplayvalue = YaxisplotHight[yval - 1] + ((YaxisplotData[yval - 1] - intDataValue[ival + 1]) * 6); Ystartval = Convert.ToInt32(finaldisplayvalue); YEndval = historgramHeigt - Convert.ToInt32(finaldisplayvalue); } else if (intDataValue[ival + 1] <= YaxisplotData[yval - 1] && intDataValue[ival + 1] < YaxisplotData[yval]) { Double finaldisplayvalue = YaxisplotHight[yval - 1] + ((YaxisplotData[yval - 1] - intDataValue[ival + 1]) * 6); Ystartval = Convert.ToInt32(finaldisplayvalue); YEndval = historgramHeigt - Convert.ToInt32(finaldisplayvalue); } } if (YEndval > 2) { } else { Ystartval = historgramHeigt - 2; YEndval = 2; } XvalPosition_Start = Convert.ToInt32(XaxisplotWidth[ival + 1]); if (XvalPosition_Start + XvalPosition_new > historgramWidth) { XvalPosition_new = XvalPosition_new - (XvalPosition_Start + XvalPosition_new - historgramWidth); } // a6 = new LinearGradientBrush(new RectangleF(StartXval, Ystartval, XvalPosition_new, YEndval), Color.GreenYellow, Color.Green, LinearGradientMode.Vertical); e.DrawRectangle(B1pen, XvalPosition_Start, Ystartval, XvalPosition_new, YEndval); e.FillRectangle(a6, XvalPosition_Start, Ystartval - 1, XvalPosition_new - 1, YEndval - 1); e.DrawString(intDataValue[ival + 1].ToString(), f2, a2, XvalPosition_Start + 10, Ystartval - 20); } } //Draw Line Curve on pareto Chart drawParetoCurveLine(e); } catch (Exception ex) { MessageBox.Show(ex.ToString()); } }
与上面的函数相同,我检查所有累积频率百分比数据,并使用
drawline
和fillpie
方法绘制折线图。public void drawParetoCurveLine(Graphics e) { First_chartDatarectHeight = PicBox.Height - 20; First_chartDatarectWidth = PicBox.Width - 20; int historgramHeigt = First_chartDatarectHeight - 30; int historgramWidth = First_chartDatarectWidth - 20; int StartXval = historgramWidth + 12; int startyval = 60; Double yValmaxDataVal = 100; Double yValminDataVal = 0; Double yValresultMaxMin = yValmaxDataVal - yValminDataVal; int NoofPlots = 5; Double yValMeasurementYAxisValue = yValmaxDataVal / NoofPlots; int yValheightcalculation = historgramHeigt / NoofPlots; int yValXvalPosition = startyval;// yValheightcalculation; Double plotYvals = yValmaxDataVal; Double[] YaxisplotHight = new Double[NoofPlots]; Double[] YaxisplotData = new Double[NoofPlots]; for (int ival = 0; ival <= NoofPlots - 1; ival++) { if (ival == NoofPlots) { e.DrawString("0", f10, a2, StartXval - 12, yValXvalPosition + 4); } else { e.DrawLine(B1pen, StartXval - 10, yValXvalPosition, StartXval, yValXvalPosition); e.DrawString(Math.Round(plotYvals, 0).ToString() + "%", f10, a2, StartXval - 11, yValXvalPosition + 4); } YaxisplotData[ival] = plotYvals; plotYvals = plotYvals - yValMeasurementYAxisValue; YaxisplotHight[ival] = yValXvalPosition; yValXvalPosition = yValXvalPosition + yValheightcalculation; } SolidBrush brush = new SolidBrush(Color.Aquamarine); Random rnd = new Random(); NoofPlots = intCumulativeValuePer.Length; int widthcalculation_new = historgramWidth / NoofPlots; int XvalPosition_Start = Convert.ToInt32(XaxisplotWidth[0]); int XvalPosition_new = Convert.ToInt32(XaxisplotWidth[1]) - XvalPosition_Start; int Ystartval = 100; int YEndval = 100; //Fill Rectangle LinearGradientBrush a6 = new LinearGradientBrush (new RectangleF(StartXval, Ystartval, widthcalculation_new - StartXval, YEndval), Color.GreenYellow, Color.Green, LinearGradientMode.Vertical); brush = new SolidBrush(Color.Aquamarine); Pen pen = new Pen(Color.Gray); Font f2 = new Font("arial", 12, FontStyle.Bold, GraphicsUnit.Pixel); Point p1 = new Point(); Point p2 = new Point(); int smallLength = (historgramWidth / (intCumulativeValuePer.Length + 1)); Double smallHeight = 0; // int smallX = topX; for (int i = 0; i < intCumulativeValuePer.Length - 1; i++) { brush.Color = Color.FromArgb(rnd.Next(200, 255), rnd.Next(255), rnd.Next(255), rnd.Next(255)); p1 = p2; if (i == 0) { p2.X = p2.X + smallLength + 40; } else { p2.X = p2.X + smallLength + 14; } smallHeight = YaxisplotHight[YaxisplotHight.Length - 1] + ((YaxisplotData[YaxisplotHight.Length - 1] - intCumulativeValuePer[i]) * 2); for (int yval = 1; yval < YaxisplotData.Length; yval++) { // Double finaldisplayvalue = YaxisplotHight[yval - 1] + // ((YaxisplotData[yval - 1] - intCumulativeValuePer[i])*10); Double finaldisplayvalue = YaxisplotHight[yval] + ((YaxisplotData[yval] - intCumulativeValuePer[i]) * 2); if (intCumulativeValuePer[i] <= YaxisplotData[yval - 1] && intCumulativeValuePer[i] > YaxisplotData[yval]) { Ystartval = Convert.ToInt32(finaldisplayvalue); smallHeight = Convert.ToInt32(finaldisplayvalue); } } // smallHeight = historgramHeigt - YaxisplotHight[i]+150; p2.Y = Convert.ToInt32(smallHeight); if (p1.X != 0 && p1.Y != 0) { e.DrawLine(pen, p1, p2); } Color pointColor = new Color(); pointColor = Color.Green; DrawDots(e, p2, pointColor); e.DrawString(Math.Round(intCumulativeValuePer[i], 2).ToString(), f2, a2, p2.X, p2.Y - 18); if (i == 0) { smallLength = smallLength - 15; } if (i == intCumulativeValuePer.Length - 2) { p1.X = p2.X; p1.Y = p2.Y; for (int yval = 1; yval < YaxisplotData.Length; yval++) { // Double finaldisplayvalue = YaxisplotHight[yval - 1] + // ((YaxisplotData[yval - 1] - // intCumulativeValuePer[i])*10); Double finaldisplayvalue = YaxisplotHight[yval] + ((YaxisplotData[yval] - intCumulativeValuePer[i + 1]) * 3); if (intCumulativeValuePer[i + 1] <= YaxisplotData[yval - 1] && intCumulativeValuePer[i + 1] > YaxisplotData[yval]) { Ystartval = Convert.ToInt32(finaldisplayvalue); smallHeight = Convert.ToInt32(finaldisplayvalue); } } // p2.X = p2.X + smallLength - 24; p2.Y = Convert.ToInt32(smallHeight); if (p1.X != 0 && p1.Y != 0) { p2.X = p2.X + smallLength + 14; e.DrawLine(pen, p1, p2); } DrawDots(e, p2, pointColor); e.DrawString(Math.Round(intCumulativeValuePer[i + 1], 2). ToString(), f2, a2, p2.X, p2.Y - 18); } } }
我添加了一个用户友好的功能,可以将我的 Pareto Chart 保存为
Image
。用户可以通过双击 Pareto Chart 控件或右键单击并选择保存来将 Pareto Chart 保存为Image
。private void PicBox_DoubleClick(object sender, EventArgs e) { saveImages(); } private void docmenu_Click(object sender, EventArgs e) { saveImages(); } public void saveImages() { if (dt.Rows.Count <= 0) { return; } using (var bitmap = new Bitmap(PicBox.Width, PicBox.Height)) { PicBox.DrawToBitmap(bitmap, PicBox.ClientRectangle); SaveFileDialog dlg = new SaveFileDialog(); dlg.FileName = "*"; dlg.DefaultExt = "bmp"; dlg.ValidateNames = true; dlg.Filter = "Bitmap Image (.bmp)|*.bmp|Gif Image (.gif)|*.gif| JPEG Image (.jpeg)|*.jpeg|Png Image (.png)|*.png"; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) { bitmap.Save(dlg.FileName); }} }
- 完成保存后,构建并运行项目。
- 现在我们创建一个 Windows 应用程序并测试我们的 "
ShanuParetoChart_CTL
" 用户控件。- 创建一个新的 Windows 项目。
- 打开您的窗体,然后在 工具箱 > 右键单击 > 选择项 > 浏览选择您的用户控件 DLL,然后 添加。
- 将用户控件拖到您的 Windows 窗体上。
- 创建
DataTable
并将Datatable
传递给我们的用户控件。public void loadGridColums() { dt.Columns.Add("No"); dt.Columns.Add("Names"); dt.Columns.Add("Frequency"); dt.Columns["Frequency"].DataType = Type.GetType("System.Int32"); }
- 在我的演示程序中,我使用了
Timer
来生成随机样本数据。我使用了 "button1
" 作为切换按钮。第一次点击时启用并启动 Timer,再次点击同一个按钮时,停止 Timer。当 TimerStart
时,我生成一个随机数并将不同的数据传递给用户控件,并检查图表结果。private void frmShanuPaerto_Load(object sender, EventArgs e) { loadgrid(1); shanuParetoChart.Bindgrid(dt); } private void button1_Click(object sender, EventArgs e) { if (button1.Text == "Real Time Data ON") { timer1.Enabled = true; timer1.Start(); button1.Text = "Real Time Data Off"; } else { timer1.Enabled = false; timer1.Stop(); button1.Text = "Real Time Data ON"; } } private void timer1_Tick(object sender, EventArgs e) { loadgrid(2); shanuParetoChart.Bindgrid(dt); }
结论
本文的主要目的是创建一个简单易用的 Pareto chart 控件,该控件可以为许多用户提供帮助,让他们可以在自己的项目中免费使用和工作。
参考链接
- 如何在 MS Excel 2010 中创建 Pareto Chart
- 什么是 Pareto Chart?
- Pareto Chart (Pareto distribution diagram) 如果您想了解更多关于 Pareto Chart 的信息,请参考此网站。该网站对 Pareto Chart 有简单的解释。
- http://www.vectorstudy.com/management-theories/pareto-chart
- http://thequalityweb.com/pareto.html
历史
- 2014 年 8 月 1 日:首次发布
- 2014 年 8 月 5 日:添加了从 framework 4.0 到 2.0 的新降级版本 (有用户要求为 Framework 2.0 提供相同的控件)。
- SHANUParetoChart_FRAMEWORK2 -> Framework 2.0
- SHANUParetoChart.zip -> Framework 4.0