Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const (
WireframeContour
Bubble
Bubble3D
StockHighLowClose
StockOpenHighLowClose
)

// ChartDashType is the type of supported chart dash types.
Expand Down Expand Up @@ -365,6 +367,8 @@ var (
WireframeContour: "General",
Bubble: "General",
Bubble3D: "General",
StockHighLowClose: "General",
StockOpenHighLowClose: "General",
}
chartValAxCrossBetween = map[ChartType]string{
Area: "midCat",
Expand Down Expand Up @@ -422,6 +426,8 @@ var (
WireframeContour: "midCat",
Bubble: "midCat",
Bubble3D: "midCat",
StockHighLowClose: "between",
StockOpenHighLowClose: "between",
}
plotAreaChartGrouping = map[ChartType]string{
Area: "standard",
Expand Down Expand Up @@ -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.
Expand Down
113 changes: 110 additions & 3 deletions chart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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())

Expand Down
60 changes: 55 additions & 5 deletions drawing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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]
}
Expand Down Expand Up @@ -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)},
Expand All @@ -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]
}

Expand Down
22 changes: 22 additions & 0 deletions xmlChart.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand All @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand All @@ -625,6 +645,8 @@ type ChartPlotArea struct {
ShowSerName bool
ShowVal bool
Fill Fill
UpBars ChartUpDownBar
DownBars ChartUpDownBar
NumFmt ChartNumFmt
}

Expand Down
Loading