diff --git a/chart.go b/chart.go index 26d60c1e35..3cc11182a1 100644 --- a/chart.go +++ b/chart.go @@ -78,6 +78,8 @@ const ( WireframeContour Bubble Bubble3D + StockHighLowClose + StockOpenHighLowClose ) // ChartDashType is the type of supported chart dash types. @@ -365,6 +367,8 @@ var ( WireframeContour: "General", Bubble: "General", Bubble3D: "General", + StockHighLowClose: "General", + StockOpenHighLowClose: "General", } chartValAxCrossBetween = map[ChartType]string{ Area: "midCat", @@ -422,6 +426,8 @@ var ( WireframeContour: "midCat", Bubble: "midCat", Bubble3D: "midCat", + StockHighLowClose: "between", + StockOpenHighLowClose: "between", } plotAreaChartGrouping = map[ChartType]string{ Area: "standard", @@ -768,6 +774,8 @@ func (opts *Chart) parseTitle() { // 52 | WireframeContour | wireframe contour chart // 53 | Bubble | bubble chart // 54 | Bubble3D | 3D bubble chart +// 55 | StockHighLowClose | High-Low-Close stock chart +// 56 | StockOpenHighLowClose | Open-High-Low-Close stock chart // // In Excel a chart series is a collection of information that defines which // data is plotted such as values, axis labels and formatting. diff --git a/chart_test.go b/chart_test.go index 7229378cf7..ad2b564d34 100644 --- a/chart_test.go +++ b/chart_test.go @@ -312,11 +312,11 @@ func TestAddChart(t *testing.T) { // Test with illegal cell reference assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test with unsupported chart type - assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error()) + assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x39, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x39).Error()) // Test add combo chart with invalid format set assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error()) // Test add combo chart with unsupported chart type - assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x37, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x37).Error()) + assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x39, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x39).Error()) // Test add chart with series transparency value exceeds limit assert.Equal(t, ErrTransparency, f.AddChart("Sheet1", "BD64", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Fill: Fill{Transparency: 110}}}})) // Test add chart with transparency value exceeds limit @@ -327,6 +327,113 @@ func TestAddChart(t *testing.T) { f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8") + + t.Run("for_create_stock_chart", func(t *testing.T) { + f := NewFile() + for i, row := range [][]interface{}{ + {"Date", "Volume", "Open", "High", "Low", "Close"}, + {45593, 14864000, 431.66, 431.94, 426.3, 426.59}, + {45590, 16899100, 426.76, 432.52, 426.57, 428.15}, + {45589, 13581600, 425.33, 425.98, 422.4, 424.73}, + {45588, 19654400, 430.86, 431.08, 422.53, 424.6}, + {45587, 25482200, 418.49, 430.58, 418.04, 427.51}, + {45586, 14206100, 416.12, 418.96, 413.75, 418.78}, + {45583, 17145300, 417.14, 419.65, 416.26, 418.16}, + {45582, 14820000, 422.36, 422.5, 415.59, 416.72}, + {45581, 15508900, 415.17, 416.36, 410.48, 416.12}, + {45580, 18900200, 422.18, 422.48, 415.26, 418.74}, + {45579, 16653100, 417.77, 424.04, 417.52, 419.14}, + {45576, 14144900, 416.14, 417.13, 413.25, 416.32}, + {45575, 13848400, 415.23, 417.35, 413.15, 415.84}, + {45574, 14974300, 415.86, 420.38, 414.3, 417.46}, + {45573, 19229300, 410.9, 415.66, 408.17, 414.71}, + {45572, 20919800, 416, 417.11, 409, 409.54}, + {45569, 19169700, 418.24, 419.75, 414.97, 416.06}, + {45568, 13686400, 417.63, 419.55, 414.29, 416.54}, + {45567, 16582300, 422.58, 422.82, 416.71, 417.13}, + {45566, 19092900, 428.45, 428.48, 418.81, 420.69}, + {45565, 16807300, 428.21, 430.42, 425.37, 430.3}, + } { + cell, err := CoordinatesToCellName(1, i+1) + assert.NoError(t, err) + assert.NoError(t, f.SetSheetRow("Sheet1", cell, &row)) + } + style, err := f.NewStyle(&Style{NumFmt: 15}) + assert.NoError(t, err) + assert.NoError(t, f.SetColStyle("Sheet1", "A", style)) + + assert.NoError(t, f.AddChart("Sheet1", "G1", &Chart{ + Type: StockHighLowClose, + Series: []ChartSeries{ + {Name: "Sheet1!$D$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$D$2:$D$22"}, + {Name: "Sheet1!$E$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$E$2:$E$22"}, + {Name: "Sheet1!$F$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$F$2:$F$22"}, + }, + Legend: ChartLegend{Position: "none"}, + Title: []RichTextRun{{Text: "High-Low-Close Stock Chart"}}, + XAxis: ChartAxis{NumFmt: ChartNumFmt{CustomNumFmt: "d-mmm-yy"}}, + })) + assert.NoError(t, f.AddChart("Sheet1", "G16", &Chart{ + Type: StockOpenHighLowClose, + Series: []ChartSeries{ + {Name: "Sheet1!$C$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$C$2:$C$22"}, + {Name: "Sheet1!$D$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$D$2:$D$22"}, + {Name: "Sheet1!$E$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$E$2:$E$22"}, + {Name: "Sheet1!$F$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$F$2:$F$22"}, + }, + Legend: ChartLegend{Position: "none"}, + Title: []RichTextRun{{Text: "Open-High-Low-Close Stock Chart"}}, + XAxis: ChartAxis{NumFmt: ChartNumFmt{CustomNumFmt: "d-mmm-yy"}}, + PlotArea: ChartPlotArea{ + UpBars: ChartUpDownBar{ + Border: ChartLine{Type: ChartLineNone}, + Fill: Fill{Type: "pattern", Color: []string{"00B050"}, Pattern: 1}, + }, + DownBars: ChartUpDownBar{ + Border: ChartLine{Type: ChartLineNone}, + Fill: Fill{Type: "pattern", Color: []string{"FF0000"}, Pattern: 1}, + }, + }, + })) + assert.NoError(t, f.AddChart("Sheet1", "O1", &Chart{ + Type: Col, + Series: []ChartSeries{ + {Name: "Sheet1!$B$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$B$2:$B$22"}, + }, + VaryColors: boolPtr(false), + XAxis: ChartAxis{NumFmt: ChartNumFmt{CustomNumFmt: "d-mmm-yy"}}, + YAxis: ChartAxis{NumFmt: ChartNumFmt{CustomNumFmt: "#,##0"}}, + Title: []RichTextRun{{Text: "Volume-High-Low-Close Stock Chart"}}, + }, &Chart{ + Type: StockHighLowClose, + Series: []ChartSeries{ + {Name: "Sheet1!$D$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$D$2:$D$22"}, + {Name: "Sheet1!$E$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$E$2:$E$22"}, + {Name: "Sheet1!$F$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$F$2:$F$22"}, + }, + YAxis: ChartAxis{Secondary: true}, + })) + assert.NoError(t, f.AddChart("Sheet1", "O16", &Chart{ + Type: Col, + Series: []ChartSeries{ + {Name: "Sheet1!$B$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$B$2:$B$22"}, + }, + VaryColors: boolPtr(false), + XAxis: ChartAxis{NumFmt: ChartNumFmt{CustomNumFmt: "d-mmm-yy"}}, + YAxis: ChartAxis{NumFmt: ChartNumFmt{CustomNumFmt: "#,##0"}}, + Title: []RichTextRun{{Text: "Volume-Open-High-Low-Close Stock Chart"}}, + }, &Chart{ + Type: StockOpenHighLowClose, + Series: []ChartSeries{ + {Name: "Sheet1!$C$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$C$2:$C$22"}, + {Name: "Sheet1!$D$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$D$2:$D$22"}, + {Name: "Sheet1!$E$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$E$2:$E$22"}, + {Name: "Sheet1!$F$1", Categories: "Sheet1!$A$2:$A$22", Values: "Sheet1!$F$2:$F$22"}, + }, + YAxis: ChartAxis{Secondary: true}, + })) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChartStock.xlsx"))) + }) } func TestAddChartSheet(t *testing.T) { @@ -363,7 +470,7 @@ func TestAddChartSheet(t *testing.T) { // Test add chartsheet with invalid sheet name assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrSheetNameInvalid.Error()) // Test with unsupported chart type - assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), newUnsupportedChartType(0x37).Error()) + assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x39, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), newUnsupportedChartType(0x39).Error()) assert.NoError(t, f.UpdateLinkedValue()) diff --git a/drawing.go b/drawing.go index be57c54f5f..bb51d91b26 100644 --- a/drawing.go +++ b/drawing.go @@ -159,6 +159,8 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { WireframeContour: f.drawSurfaceChart, Bubble: f.drawBubbleChart, Bubble3D: f.drawBubbleChart, + StockHighLowClose: f.drawStockChart, + StockOpenHighLowClose: f.drawStockChart, } xlsxChartSpace.Chart.drawChartLegend(opts) xlsxChartSpace.Chart.PlotArea.SpPr = f.drawShapeFill(opts.PlotArea.Fill, xlsxChartSpace.Chart.PlotArea.SpPr) @@ -171,7 +173,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { continue } fld := immutable.FieldByName(mutable.Type().Field(i).Name) - if field.Kind() == reflect.Slice && i < 16 { // All []*cCharts type fields + if field.Kind() == reflect.Slice && i < 17 { // All []*cCharts type fields fld.Set(reflect.Append(fld, field.Index(0))) continue } @@ -185,6 +187,10 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](xlsxChartSpace.Chart.PlotArea, comboCharts[idx])) order += len(comboCharts[idx].Series) } + // If the dateAx field exists, valAx field should be nil. + if xlsxChartSpace.Chart.PlotArea != nil && xlsxChartSpace.Chart.PlotArea.DateAx != nil { + xlsxChartSpace.Chart.PlotArea.CatAx = nil + } chart, _ := xml.Marshal(xlsxChartSpace) media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" f.saveFileList(media, chart) @@ -689,6 +695,39 @@ func (f *File) drawBubbleChart(pa *cPlotArea, opts *Chart) *cPlotArea { return plotArea } +// drawStockChart provides a function to draw the c:stockChart element by +// given format sets. +func (f *File) drawStockChart(pa *cPlotArea, opts *Chart) *cPlotArea { + plotArea := &cPlotArea{ + StockChart: []*cCharts{ + { + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), + }, + }, + ValAx: f.drawPlotAreaValAx(pa, opts), + DateAx: f.drawPlotAreaCatAx(pa, opts), + } + if opts.Type == StockHighLowClose { + plotArea.StockChart[0].HiLowLines = &cChartLines{} + } + if opts.Type == StockOpenHighLowClose { + plotArea.StockChart[0].HiLowLines = &cChartLines{} + plotArea.StockChart[0].UpDownBars = &cUpDownBars{ + GapWidth: &attrValString{Val: stringPtr("150")}, + UpBars: &cChartLines{f.drawShapeFill(opts.PlotArea.UpBars.Fill, &cSpPr{Ln: f.drawChartLn(&opts.PlotArea.UpBars.Border)})}, + DownBars: &cChartLines{f.drawShapeFill(opts.PlotArea.DownBars.Fill, &cSpPr{Ln: f.drawChartLn(&opts.PlotArea.UpBars.Border)})}, + } + } + ser := *plotArea.StockChart[0].Ser + ser[0].Val.NumRef.NumCache = &cNumCache{} + return plotArea +} + // drawChartGapWidth provides a function to draw the c:gapWidth element by given // format sets. func (f *File) drawChartGapWidth(opts *Chart) *attrValInt { @@ -818,8 +857,10 @@ func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { } noLn := &cSpPr{Ln: &aLn{NoFill: &attrValString{}}} if chartSeriesSpPr, ok := map[ChartType]map[ChartLineType]*cSpPr{ - Line: {ChartLineUnset: solid, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: solid}, - Scatter: {ChartLineUnset: noLn, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: noLn}, + Line: {ChartLineUnset: solid, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: solid}, + Scatter: {ChartLineUnset: noLn, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: noLn}, + StockHighLowClose: {ChartLineUnset: noLn, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: noLn}, + StockOpenHighLowClose: {ChartLineUnset: noLn, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: noLn}, }[opts.Type]; ok { return chartSeriesSpPr[opts.Series[i].Line.Type] } @@ -892,7 +933,11 @@ func (f *File) drawChartSeriesVal(v ChartSeries, opts *Chart) *cVal { // drawChartSeriesMarker provides a function to draw the c:marker element by // given data index and format sets. func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker { - defaultSymbol := map[ChartType]*attrValString{Scatter: {Val: stringPtr("circle")}} + defaultSymbol := map[ChartType]*attrValString{ + Scatter: {Val: stringPtr("circle")}, + StockHighLowClose: {Val: stringPtr("dot")}, + StockOpenHighLowClose: {Val: stringPtr("none")}, + } marker := &cMarker{ Symbol: defaultSymbol[opts.Type], Size: &attrValInt{Val: intPtr(5)}, @@ -912,7 +957,12 @@ func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker { if marker.SpPr != nil && marker.SpPr.Ln != nil { marker.SpPr.Ln = f.drawChartLn(&opts.Series[i].Marker.Border) } - chartSeriesMarker := map[ChartType]*cMarker{Scatter: marker, Line: marker} + chartSeriesMarker := map[ChartType]*cMarker{ + Scatter: marker, + Line: marker, + StockHighLowClose: marker, + StockOpenHighLowClose: marker, + } return chartSeriesMarker[opts.Type] } diff --git a/xmlChart.go b/xmlChart.go index adf0060426..518a19f413 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -356,6 +356,7 @@ type cPlotArea struct { DoughnutChart []*cCharts `xml:"doughnutChart"` LineChart []*cCharts `xml:"lineChart"` Line3DChart []*cCharts `xml:"line3DChart"` + StockChart []*cCharts `xml:"stockChart"` PieChart []*cCharts `xml:"pieChart"` Pie3DChart []*cCharts `xml:"pie3DChart"` OfPieChart []*cCharts `xml:"ofPieChart"` @@ -365,6 +366,7 @@ type cPlotArea struct { SurfaceChart []*cCharts `xml:"surfaceChart"` CatAx []*cAxs `xml:"catAx"` ValAx []*cAxs `xml:"valAx"` + DateAx []*cAxs `xml:"dateAx"` SerAx []*cAxs `xml:"serAx"` DTable *cDTable `xml:"dTable"` SpPr *cSpPr `xml:"spPr"` @@ -384,6 +386,8 @@ type cCharts struct { SplitPos *attrValInt `xml:"splitPos"` SerLines *attrValString `xml:"serLines"` DLbls *cDLbls `xml:"dLbls"` + HiLowLines *cChartLines `xml:"hiLowLines"` + UpDownBars *cUpDownBars `xml:"upDownBars"` GapWidth *attrValInt `xml:"gapWidth"` Shape *attrValString `xml:"shape"` HoleSize *attrValInt `xml:"holeSize"` @@ -420,6 +424,15 @@ type cAxs struct { NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` } +// cUpDownBars directly maps the upDownBars lement. This element specifies +// the up and down bars. +type cUpDownBars struct { + GapWidth *attrValString `xml:"gapWidth"` + UpBars *cChartLines `xml:"upBars"` + DownBars *cChartLines `xml:"downBars"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + // cChartLines directly maps the chart lines content model. type cChartLines struct { SpPr *cSpPr `xml:"spPr"` @@ -613,6 +626,13 @@ type ChartDimension struct { Height uint } +// ChartUpDownBar directly maps the format settings of the stock chart up bars +// and down bars. +type ChartUpDownBar struct { + Fill Fill + Border ChartLine +} + // ChartPlotArea directly maps the format settings of the plot area. type ChartPlotArea struct { SecondPlotValues int @@ -625,6 +645,8 @@ type ChartPlotArea struct { ShowSerName bool ShowVal bool Fill Fill + UpBars ChartUpDownBar + DownBars ChartUpDownBar NumFmt ChartNumFmt }