excelize/drawing.go

1414 lines
40 KiB
Go

// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later.
package excelize
import (
"bytes"
"encoding/xml"
"io"
"reflect"
"strconv"
"strings"
)
// prepareDrawing provides a function to prepare drawing ID and XML by given
// drawingID, worksheet name and default drawingXML.
func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) {
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
if ws.Drawing != nil {
// The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml.
sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml"))
drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl")
} else {
// Add first picture for given sheet.
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
f.addSheetDrawing(sheet, rID)
}
return drawingID, drawingXML
}
// prepareChartSheetDrawing provides a function to prepare drawing ID and XML
// by given drawingID, worksheet name and default drawingXML.
func (f *File) prepareChartSheetDrawing(cs *xlsxChartsheet, drawingID int, sheet string) {
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
// Only allow one chart in a chartsheet.
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/chartsheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
f.addSheetNameSpace(sheet, SourceRelationship)
cs.Drawing = &xlsxDrawing{
RID: "rId" + strconv.Itoa(rID),
}
}
// addChart provides a function to create chart as xl/charts/chart%d.xml by
// given format sets.
func (f *File) addChart(opts *Chart, comboCharts []*Chart) {
count := f.countCharts()
xlsxChartSpace := xlsxChartSpace{
XMLNSa: NameSpaceDrawingML.Value,
Date1904: &attrValBool{Val: boolPtr(false)},
Lang: &attrValString{Val: stringPtr("en-US")},
RoundedCorners: &attrValBool{Val: boolPtr(false)},
Chart: cChart{
Title: &cTitle{
Tx: cTx{
Rich: &cRich{
P: aP{
PPr: &aPPr{
DefRPr: aRPr{
Kern: 1200,
Strike: "noStrike",
U: "none",
Sz: 1400,
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "tx1",
LumMod: &attrValInt{
Val: intPtr(65000),
},
LumOff: &attrValInt{
Val: intPtr(35000),
},
},
},
Ea: &aEa{
Typeface: "+mn-ea",
},
Cs: &aCs{
Typeface: "+mn-cs",
},
Latin: &xlsxCTTextFont{
Typeface: "+mn-lt",
},
},
},
R: &aR{
RPr: aRPr{
Lang: "en-US",
AltLang: "en-US",
},
T: opts.Title.Name,
},
},
},
},
TxPr: cTxPr{
P: aP{
PPr: &aPPr{
DefRPr: aRPr{
Kern: 1200,
U: "none",
Sz: 14000,
Strike: "noStrike",
},
},
EndParaRPr: &aEndParaRPr{
Lang: "en-US",
},
},
},
Overlay: &attrValBool{Val: boolPtr(false)},
},
View3D: &cView3D{
RotX: &attrValInt{Val: intPtr(chartView3DRotX[opts.Type])},
RotY: &attrValInt{Val: intPtr(chartView3DRotY[opts.Type])},
Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[opts.Type])},
RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[opts.Type])},
},
Floor: &cThicknessSpPr{
Thickness: &attrValInt{Val: intPtr(0)},
},
SideWall: &cThicknessSpPr{
Thickness: &attrValInt{Val: intPtr(0)},
},
BackWall: &cThicknessSpPr{
Thickness: &attrValInt{Val: intPtr(0)},
},
PlotArea: &cPlotArea{},
Legend: &cLegend{
LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[opts.Legend.Position])},
Overlay: &attrValBool{Val: boolPtr(false)},
},
PlotVisOnly: &attrValBool{Val: boolPtr(false)},
DispBlanksAs: &attrValString{Val: stringPtr(opts.ShowBlanksAs)},
ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)},
},
SpPr: &cSpPr{
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{Val: "bg1"},
},
Ln: &aLn{
W: 9525,
Cap: "flat",
Cmpd: "sng",
Algn: "ctr",
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "tx1",
LumMod: &attrValInt{
Val: intPtr(15000),
},
LumOff: &attrValInt{
Val: intPtr(85000),
},
},
},
},
},
PrintSettings: &cPrintSettings{
PageMargins: &cPageMargins{
B: 0.75,
L: 0.7,
R: 0.7,
T: 0.7,
Header: 0.3,
Footer: 0.3,
},
},
}
plotAreaFunc := map[string]func(*Chart) *cPlotArea{
Area: f.drawBaseChart,
AreaStacked: f.drawBaseChart,
AreaPercentStacked: f.drawBaseChart,
Area3D: f.drawBaseChart,
Area3DStacked: f.drawBaseChart,
Area3DPercentStacked: f.drawBaseChart,
Bar: f.drawBaseChart,
BarStacked: f.drawBaseChart,
BarPercentStacked: f.drawBaseChart,
Bar3DClustered: f.drawBaseChart,
Bar3DStacked: f.drawBaseChart,
Bar3DPercentStacked: f.drawBaseChart,
Bar3DConeClustered: f.drawBaseChart,
Bar3DConeStacked: f.drawBaseChart,
Bar3DConePercentStacked: f.drawBaseChart,
Bar3DPyramidClustered: f.drawBaseChart,
Bar3DPyramidStacked: f.drawBaseChart,
Bar3DPyramidPercentStacked: f.drawBaseChart,
Bar3DCylinderClustered: f.drawBaseChart,
Bar3DCylinderStacked: f.drawBaseChart,
Bar3DCylinderPercentStacked: f.drawBaseChart,
Col: f.drawBaseChart,
ColStacked: f.drawBaseChart,
ColPercentStacked: f.drawBaseChart,
Col3D: f.drawBaseChart,
Col3DClustered: f.drawBaseChart,
Col3DStacked: f.drawBaseChart,
Col3DPercentStacked: f.drawBaseChart,
Col3DCone: f.drawBaseChart,
Col3DConeClustered: f.drawBaseChart,
Col3DConeStacked: f.drawBaseChart,
Col3DConePercentStacked: f.drawBaseChart,
Col3DPyramid: f.drawBaseChart,
Col3DPyramidClustered: f.drawBaseChart,
Col3DPyramidStacked: f.drawBaseChart,
Col3DPyramidPercentStacked: f.drawBaseChart,
Col3DCylinder: f.drawBaseChart,
Col3DCylinderClustered: f.drawBaseChart,
Col3DCylinderStacked: f.drawBaseChart,
Col3DCylinderPercentStacked: f.drawBaseChart,
Doughnut: f.drawDoughnutChart,
Line: f.drawLineChart,
Line3D: f.drawLine3DChart,
Pie: f.drawPieChart,
Pie3D: f.drawPie3DChart,
PieOfPieChart: f.drawPieOfPieChart,
BarOfPieChart: f.drawBarOfPieChart,
Radar: f.drawRadarChart,
Scatter: f.drawScatterChart,
Surface3D: f.drawSurface3DChart,
WireframeSurface3D: f.drawSurface3DChart,
Contour: f.drawSurfaceChart,
WireframeContour: f.drawSurfaceChart,
Bubble: f.drawBubbleChart,
Bubble3D: f.drawBubbleChart,
}
if opts.Legend.Position == "none" {
xlsxChartSpace.Chart.Legend = nil
}
addChart := func(c, p *cPlotArea) {
immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem()
for i := 0; i < mutable.NumField(); i++ {
field := mutable.Field(i)
if field.IsNil() {
continue
}
immutable.FieldByName(mutable.Type().Field(i).Name).Set(field)
}
}
addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[opts.Type](opts))
order := len(opts.Series)
for idx := range comboCharts {
comboCharts[idx].order = order
addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx]))
order += len(comboCharts[idx].Series)
}
chart, _ := xml.Marshal(xlsxChartSpace)
media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml"
f.saveFileList(media, chart)
}
// drawBaseChart provides a function to draw the c:plotArea element for bar,
// and column series charts by given format sets.
func (f *File) drawBaseChart(opts *Chart) *cPlotArea {
c := cCharts{
BarDir: &attrValString{
Val: stringPtr("col"),
},
Grouping: &attrValString{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
Shape: f.drawChartShape(opts),
DLbls: f.drawChartDLbls(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
},
Overlap: &attrValInt{Val: intPtr(100)},
}
var ok bool
if *c.BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok {
c.BarDir = nil
}
if *c.Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok {
c.Overlap = nil
}
catAx := f.drawPlotAreaCatAx(opts)
valAx := f.drawPlotAreaValAx(opts)
charts := map[string]*cPlotArea{
"area": {
AreaChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"areaStacked": {
AreaChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"areaPercentStacked": {
AreaChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"area3D": {
Area3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"area3DStacked": {
Area3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"area3DPercentStacked": {
Area3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar": {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"barStacked": {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"barPercentStacked": {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DPercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DConeClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DConeStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DConePercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DPyramidClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DPyramidStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DPyramidPercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DCylinderClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DCylinderStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bar3DCylinderPercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col": {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"colStacked": {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"colPercentStacked": {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3D": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DPercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DCone": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DConeClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DConeStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DConePercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DPyramid": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DPyramidClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DPyramidStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DPyramidPercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DCylinder": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DCylinderClustered": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DCylinderStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"col3DCylinderPercentStacked": {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bubble": {
BubbleChart: &c,
CatAx: catAx,
ValAx: valAx,
},
"bubble3D": {
BubbleChart: &c,
CatAx: catAx,
ValAx: valAx,
},
}
return charts[opts.Type]
}
// drawDoughnutChart provides a function to draw the c:plotArea element for
// doughnut chart by given format sets.
func (f *File) drawDoughnutChart(opts *Chart) *cPlotArea {
holeSize := 75
if opts.HoleSize > 0 && opts.HoleSize <= 90 {
holeSize = opts.HoleSize
}
return &cPlotArea{
DoughnutChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
HoleSize: &attrValInt{Val: intPtr(holeSize)},
},
}
}
// drawLineChart provides a function to draw the c:plotArea element for line
// chart by given format sets.
func (f *File) drawLineChart(opts *Chart) *cPlotArea {
return &cPlotArea{
LineChart: &cCharts{
Grouping: &attrValString{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawLine3DChart provides a function to draw the c:plotArea element for line
// chart by given format sets.
func (f *File) drawLine3DChart(opts *Chart) *cPlotArea {
return &cPlotArea{
Line3DChart: &cCharts{
Grouping: &attrValString{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawPieChart provides a function to draw the c:plotArea element for pie
// chart by given format sets.
func (f *File) drawPieChart(opts *Chart) *cPlotArea {
return &cPlotArea{
PieChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
},
}
}
// drawPie3DChart provides a function to draw the c:plotArea element for 3D
// pie chart by given format sets.
func (f *File) drawPie3DChart(opts *Chart) *cPlotArea {
return &cPlotArea{
Pie3DChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
},
}
}
// drawPieOfPieChart provides a function to draw the c:plotArea element for
// pie chart by given format sets.
func (f *File) drawPieOfPieChart(opts *Chart) *cPlotArea {
return &cPlotArea{
OfPieChart: &cCharts{
OfPieType: &attrValString{
Val: stringPtr("pie"),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
SerLines: &attrValString{},
},
}
}
// drawBarOfPieChart provides a function to draw the c:plotArea element for
// pie chart by given format sets.
func (f *File) drawBarOfPieChart(opts *Chart) *cPlotArea {
return &cPlotArea{
OfPieChart: &cCharts{
OfPieType: &attrValString{
Val: stringPtr("bar"),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
SerLines: &attrValString{},
},
}
}
// drawRadarChart provides a function to draw the c:plotArea element for radar
// chart by given format sets.
func (f *File) drawRadarChart(opts *Chart) *cPlotArea {
return &cPlotArea{
RadarChart: &cCharts{
RadarStyle: &attrValString{
Val: stringPtr("marker"),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawScatterChart provides a function to draw the c:plotArea element for
// scatter chart by given format sets.
func (f *File) drawScatterChart(opts *Chart) *cPlotArea {
return &cPlotArea{
ScatterChart: &cCharts{
ScatterStyle: &attrValString{
Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawSurface3DChart provides a function to draw the c:surface3DChart element by
// given format sets.
func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
Surface3DChart: &cCharts{
Ser: f.drawChartSeries(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
{Val: intPtr(832256642)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
SerAx: f.drawPlotAreaSerAx(opts),
}
if opts.Type == WireframeSurface3D {
plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)}
}
return plotArea
}
// drawSurfaceChart provides a function to draw the c:surfaceChart element by
// given format sets.
func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
SurfaceChart: &cCharts{
Ser: f.drawChartSeries(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
{Val: intPtr(832256642)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
SerAx: f.drawPlotAreaSerAx(opts),
}
if opts.Type == WireframeContour {
plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)}
}
return plotArea
}
// drawBubbleChart provides a function to draw the c:bubbleChart element by
// given format sets.
func (f *File) drawBubbleChart(opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
BubbleChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: []*attrValInt{
{Val: intPtr(754001152)},
{Val: intPtr(753999904)},
},
},
ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]},
}
return plotArea
}
// drawChartShape provides a function to draw the c:shape element by given
// format sets.
func (f *File) drawChartShape(opts *Chart) *attrValString {
shapes := map[string]string{
Bar3DConeClustered: "cone",
Bar3DConeStacked: "cone",
Bar3DConePercentStacked: "cone",
Bar3DPyramidClustered: "pyramid",
Bar3DPyramidStacked: "pyramid",
Bar3DPyramidPercentStacked: "pyramid",
Bar3DCylinderClustered: "cylinder",
Bar3DCylinderStacked: "cylinder",
Bar3DCylinderPercentStacked: "cylinder",
Col3DCone: "cone",
Col3DConeClustered: "cone",
Col3DConeStacked: "cone",
Col3DConePercentStacked: "cone",
Col3DPyramid: "pyramid",
Col3DPyramidClustered: "pyramid",
Col3DPyramidStacked: "pyramid",
Col3DPyramidPercentStacked: "pyramid",
Col3DCylinder: "cylinder",
Col3DCylinderClustered: "cylinder",
Col3DCylinderStacked: "cylinder",
Col3DCylinderPercentStacked: "cylinder",
}
if shape, ok := shapes[opts.Type]; ok {
return &attrValString{Val: stringPtr(shape)}
}
return nil
}
// drawChartSeries provides a function to draw the c:ser element by given
// format sets.
func (f *File) drawChartSeries(opts *Chart) *[]cSer {
var ser []cSer
for k := range opts.Series {
ser = append(ser, cSer{
IDx: &attrValInt{Val: intPtr(k + opts.order)},
Order: &attrValInt{Val: intPtr(k + opts.order)},
Tx: &cTx{
StrRef: &cStrRef{
F: opts.Series[k].Name,
},
},
SpPr: f.drawChartSeriesSpPr(k, opts),
Marker: f.drawChartSeriesMarker(k, opts),
DPt: f.drawChartSeriesDPt(k, opts),
DLbls: f.drawChartSeriesDLbls(opts),
InvertIfNegative: &attrValBool{Val: boolPtr(false)},
Cat: f.drawChartSeriesCat(opts.Series[k], opts),
Smooth: &attrValBool{Val: boolPtr(opts.Series[k].Line.Smooth)},
Val: f.drawChartSeriesVal(opts.Series[k], opts),
XVal: f.drawChartSeriesXVal(opts.Series[k], opts),
YVal: f.drawChartSeriesYVal(opts.Series[k], opts),
BubbleSize: f.drawCharSeriesBubbleSize(opts.Series[k], opts),
Bubble3D: f.drawCharSeriesBubble3D(opts),
})
}
return &ser
}
// drawChartSeriesSpPr provides a function to draw the c:spPr element by given
// format sets.
func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr {
var srgbClr *attrValString
var schemeClr *aSchemeClr
if color := opts.Series[i].Fill.Color; len(color) == 1 {
srgbClr = &attrValString{Val: stringPtr(strings.TrimPrefix(color[0], "#"))}
} else {
schemeClr = &aSchemeClr{Val: "accent" + strconv.Itoa((opts.order+i)%6+1)}
}
spPr := &cSpPr{SolidFill: &aSolidFill{SchemeClr: schemeClr, SrgbClr: srgbClr}}
spPrScatter := &cSpPr{
Ln: &aLn{
W: 25400,
NoFill: " ",
},
}
spPrLine := &cSpPr{
Ln: &aLn{
W: f.ptToEMUs(opts.Series[i].Line.Width),
Cap: "rnd", // rnd, sq, flat
SolidFill: &aSolidFill{
SchemeClr: schemeClr,
SrgbClr: srgbClr,
},
},
}
if chartSeriesSpPr, ok := map[string]*cSpPr{
Line: spPrLine, Scatter: spPrScatter,
}[opts.Type]; ok {
return chartSeriesSpPr
}
if srgbClr != nil {
return spPr
}
return nil
}
// drawChartSeriesDPt provides a function to draw the c:dPt element by given
// data index and format sets.
func (f *File) drawChartSeriesDPt(i int, opts *Chart) []*cDPt {
dpt := []*cDPt{{
IDx: &attrValInt{Val: intPtr(i)},
Bubble3D: &attrValBool{Val: boolPtr(false)},
SpPr: &cSpPr{
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)},
},
Ln: &aLn{
W: 25400,
Cap: "rnd",
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)},
},
},
Sp3D: &aSp3D{
ContourW: 25400,
ContourClr: &aContourClr{
SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)},
},
},
},
}}
chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt}
return chartSeriesDPt[opts.Type]
}
// drawChartSeriesCat provides a function to draw the c:cat element by given
// chart series and format sets.
func (f *File) drawChartSeriesCat(v ChartSeries, opts *Chart) *cCat {
cat := &cCat{
StrRef: &cStrRef{
F: v.Categories,
},
}
chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil}
if _, ok := chartSeriesCat[opts.Type]; ok || v.Categories == "" {
return nil
}
return cat
}
// drawChartSeriesVal provides a function to draw the c:val element by given
// chart series and format sets.
func (f *File) drawChartSeriesVal(v ChartSeries, opts *Chart) *cVal {
val := &cVal{
NumRef: &cNumRef{
F: v.Values,
},
}
chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil}
if _, ok := chartSeriesVal[opts.Type]; ok {
return nil
}
return val
}
// 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[string]*attrValString{Scatter: {Val: stringPtr("circle")}}
marker := &cMarker{
Symbol: defaultSymbol[opts.Type],
Size: &attrValInt{Val: intPtr(5)},
}
if symbol := stringPtr(opts.Series[i].Marker.Symbol); *symbol != "" {
marker.Symbol = &attrValString{Val: symbol}
}
if size := intPtr(opts.Series[i].Marker.Size); *size != 0 {
marker.Size = &attrValInt{Val: size}
}
if i < 6 {
marker.SpPr = &cSpPr{
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "accent" + strconv.Itoa(i+1),
},
},
Ln: &aLn{
W: 9252,
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "accent" + strconv.Itoa(i+1),
},
},
},
}
}
chartSeriesMarker := map[string]*cMarker{Scatter: marker, Line: marker}
return chartSeriesMarker[opts.Type]
}
// drawChartSeriesXVal provides a function to draw the c:xVal element by given
// chart series and format sets.
func (f *File) drawChartSeriesXVal(v ChartSeries, opts *Chart) *cCat {
cat := &cCat{
StrRef: &cStrRef{
F: v.Categories,
},
}
chartSeriesXVal := map[string]*cCat{Scatter: cat, Bubble: cat, Bubble3D: cat}
return chartSeriesXVal[opts.Type]
}
// drawChartSeriesYVal provides a function to draw the c:yVal element by given
// chart series and format sets.
func (f *File) drawChartSeriesYVal(v ChartSeries, opts *Chart) *cVal {
val := &cVal{
NumRef: &cNumRef{
F: v.Values,
},
}
chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val}
return chartSeriesYVal[opts.Type]
}
// drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize
// element by given chart series and format sets.
func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal {
if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok || v.Sizes == "" {
return nil
}
return &cVal{
NumRef: &cNumRef{
F: v.Sizes,
},
}
}
// drawCharSeriesBubble3D provides a function to draw the c:bubble3D element
// by given format sets.
func (f *File) drawCharSeriesBubble3D(opts *Chart) *attrValBool {
if _, ok := map[string]bool{Bubble3D: true}[opts.Type]; !ok {
return nil
}
return &attrValBool{Val: boolPtr(true)}
}
// drawChartDLbls provides a function to draw the c:dLbls element by given
// format sets.
func (f *File) drawChartDLbls(opts *Chart) *cDLbls {
return &cDLbls{
ShowLegendKey: &attrValBool{Val: boolPtr(opts.Legend.ShowLegendKey)},
ShowVal: &attrValBool{Val: boolPtr(opts.PlotArea.ShowVal)},
ShowCatName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowCatName)},
ShowSerName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowSerName)},
ShowBubbleSize: &attrValBool{Val: boolPtr(opts.PlotArea.ShowBubbleSize)},
ShowPercent: &attrValBool{Val: boolPtr(opts.PlotArea.ShowPercent)},
ShowLeaderLines: &attrValBool{Val: boolPtr(opts.PlotArea.ShowLeaderLines)},
}
}
// drawChartSeriesDLbls provides a function to draw the c:dLbls element by
// given format sets.
func (f *File) drawChartSeriesDLbls(opts *Chart) *cDLbls {
dLbls := f.drawChartDLbls(opts)
chartSeriesDLbls := map[string]*cDLbls{
Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil,
}
if _, ok := chartSeriesDLbls[opts.Type]; ok {
return nil
}
return dLbls
}
// drawPlotAreaCatAx provides a function to draw the c:catAx element.
func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs {
max := &attrValFloat{Val: opts.XAxis.Maximum}
min := &attrValFloat{Val: opts.XAxis.Minimum}
if opts.XAxis.Maximum == nil {
max = nil
}
if opts.XAxis.Minimum == nil {
min = nil
}
axs := []*cAxs{
{
AxID: &attrValInt{Val: intPtr(754001152)},
Scaling: &cScaling{
Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])},
Max: max,
Min: min,
},
Delete: &attrValBool{Val: boolPtr(opts.XAxis.None)},
AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])},
NumFmt: &cNumFmt{
FormatCode: "General",
SourceLinked: true,
},
MajorTickMark: &attrValString{Val: stringPtr("none")},
MinorTickMark: &attrValString{Val: stringPtr("none")},
TickLblPos: &attrValString{Val: stringPtr("nextTo")},
SpPr: f.drawPlotAreaSpPr(),
TxPr: f.drawPlotAreaTxPr(&opts.YAxis),
CrossAx: &attrValInt{Val: intPtr(753999904)},
Crosses: &attrValString{Val: stringPtr("autoZero")},
Auto: &attrValBool{Val: boolPtr(true)},
LblAlgn: &attrValString{Val: stringPtr("ctr")},
LblOffset: &attrValInt{Val: intPtr(100)},
NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)},
},
}
if opts.XAxis.MajorGridLines {
axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if opts.XAxis.MinorGridLines {
axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if opts.XAxis.TickLabelSkip != 0 {
axs[0].TickLblSkip = &attrValInt{Val: intPtr(opts.XAxis.TickLabelSkip)}
}
return axs
}
// drawPlotAreaValAx provides a function to draw the c:valAx element.
func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs {
max := &attrValFloat{Val: opts.YAxis.Maximum}
min := &attrValFloat{Val: opts.YAxis.Minimum}
if opts.YAxis.Maximum == nil {
max = nil
}
if opts.YAxis.Minimum == nil {
min = nil
}
var logBase *attrValFloat
if opts.YAxis.LogBase >= 2 && opts.YAxis.LogBase <= 1000 {
logBase = &attrValFloat{Val: float64Ptr(opts.YAxis.LogBase)}
}
axs := []*cAxs{
{
AxID: &attrValInt{Val: intPtr(753999904)},
Scaling: &cScaling{
LogBase: logBase,
Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])},
Max: max,
Min: min,
},
Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)},
AxPos: &attrValString{Val: stringPtr(valAxPos[opts.YAxis.ReverseOrder])},
NumFmt: &cNumFmt{
FormatCode: chartValAxNumFmtFormatCode[opts.Type],
SourceLinked: true,
},
MajorTickMark: &attrValString{Val: stringPtr("none")},
MinorTickMark: &attrValString{Val: stringPtr("none")},
TickLblPos: &attrValString{Val: stringPtr("nextTo")},
SpPr: f.drawPlotAreaSpPr(),
TxPr: f.drawPlotAreaTxPr(&opts.XAxis),
CrossAx: &attrValInt{Val: intPtr(754001152)},
Crosses: &attrValString{Val: stringPtr("autoZero")},
CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])},
},
}
if opts.YAxis.MajorGridLines {
axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if opts.YAxis.MinorGridLines {
axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if pos, ok := valTickLblPos[opts.Type]; ok {
axs[0].TickLblPos.Val = stringPtr(pos)
}
if opts.YAxis.MajorUnit != 0 {
axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(opts.YAxis.MajorUnit)}
}
return axs
}
// drawPlotAreaSerAx provides a function to draw the c:serAx element.
func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs {
max := &attrValFloat{Val: opts.YAxis.Maximum}
min := &attrValFloat{Val: opts.YAxis.Minimum}
if opts.YAxis.Maximum == nil {
max = nil
}
if opts.YAxis.Minimum == nil {
min = nil
}
return []*cAxs{
{
AxID: &attrValInt{Val: intPtr(832256642)},
Scaling: &cScaling{
Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])},
Max: max,
Min: min,
},
Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)},
AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])},
TickLblPos: &attrValString{Val: stringPtr("nextTo")},
SpPr: f.drawPlotAreaSpPr(),
TxPr: f.drawPlotAreaTxPr(nil),
CrossAx: &attrValInt{Val: intPtr(753999904)},
},
}
}
// drawPlotAreaSpPr provides a function to draw the c:spPr element.
func (f *File) drawPlotAreaSpPr() *cSpPr {
return &cSpPr{
Ln: &aLn{
W: 9525,
Cap: "flat",
Cmpd: "sng",
Algn: "ctr",
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "tx1",
LumMod: &attrValInt{Val: intPtr(15000)},
LumOff: &attrValInt{Val: intPtr(85000)},
},
},
},
}
}
// drawPlotAreaTxPr provides a function to draw the c:txPr element.
func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr {
cTxPr := &cTxPr{
BodyPr: aBodyPr{
Rot: -60000000,
SpcFirstLastPara: true,
VertOverflow: "ellipsis",
Vert: "horz",
Wrap: "square",
Anchor: "ctr",
AnchorCtr: true,
},
P: aP{
PPr: &aPPr{
DefRPr: aRPr{
Sz: 900,
B: false,
I: false,
U: "none",
Strike: "noStrike",
Kern: 1200,
Baseline: 0,
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "tx1",
LumMod: &attrValInt{Val: intPtr(15000)},
LumOff: &attrValInt{Val: intPtr(85000)},
},
},
Latin: &xlsxCTTextFont{Typeface: "+mn-lt"},
Ea: &aEa{Typeface: "+mn-ea"},
Cs: &aCs{Typeface: "+mn-cs"},
},
},
EndParaRPr: &aEndParaRPr{Lang: "en-US"},
},
}
if opts != nil {
cTxPr.P.PPr.DefRPr.B = opts.Font.Bold
cTxPr.P.PPr.DefRPr.I = opts.Font.Italic
if idx := inStrSlice(supportedDrawingUnderlineTypes, opts.Font.Underline, true); idx != -1 {
cTxPr.P.PPr.DefRPr.U = supportedDrawingUnderlineTypes[idx]
}
if opts.Font.Color != "" {
cTxPr.P.PPr.DefRPr.SolidFill.SchemeClr = nil
cTxPr.P.PPr.DefRPr.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(opts.Font.Color), "#", ""))}
}
}
return cTxPr
}
// drawingParser provides a function to parse drawingXML. In order to solve
// the problem that the label structure is changed after serialization and
// deserialization, two different structures: decodeWsDr and encodeWsDr are
// defined.
func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) {
var (
err error
ok bool
)
_, ok = f.Drawings.Load(path)
if !ok {
content := xlsxWsDr{}
content.A = NameSpaceDrawingML.Value
content.Xdr = NameSpaceDrawingMLSpreadSheet.Value
if _, ok = f.Pkg.Load(path); ok { // Append Model
decodeWsDr := decodeWsDr{}
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).
Decode(&decodeWsDr); err != nil && err != io.EOF {
return nil, 0, err
}
content.R = decodeWsDr.R
for _, v := range decodeWsDr.AlternateContent {
content.AlternateContent = append(content.AlternateContent, &xlsxAlternateContent{
Content: v.Content,
XMLNSMC: SourceRelationshipCompatibility.Value,
})
}
for _, v := range decodeWsDr.OneCellAnchor {
content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{
EditAs: v.EditAs,
GraphicFrame: v.Content,
})
}
for _, v := range decodeWsDr.TwoCellAnchor {
content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{
EditAs: v.EditAs,
GraphicFrame: v.Content,
})
}
}
f.Drawings.Store(path, &content)
}
var wsDr *xlsxWsDr
if drawing, ok := f.Drawings.Load(path); ok && drawing != nil {
wsDr = drawing.(*xlsxWsDr)
}
wsDr.Lock()
defer wsDr.Unlock()
return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2, nil
}
// addDrawingChart provides a function to add chart graphic frame by given
// sheet, drawingXML, cell, width, height, relationship index and format sets.
func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *GraphicOptions) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
colIdx := col - 1
rowIdx := row - 1
width = int(float64(width) * opts.ScaleX)
height = int(float64(height) * opts.ScaleY)
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.OffsetX, opts.OffsetY, width, height)
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
}
twoCellAnchor := xdrCellAnchor{}
twoCellAnchor.EditAs = opts.Positioning
from := xlsxFrom{}
from.Col = colStart
from.ColOff = opts.OffsetX * EMU
from.Row = rowStart
from.RowOff = opts.OffsetY * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
to.Row = rowEnd
to.RowOff = y2 * EMU
twoCellAnchor.From = &from
twoCellAnchor.To = &to
graphicFrame := xlsxGraphicFrame{
NvGraphicFramePr: xlsxNvGraphicFramePr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
Name: "Chart " + strconv.Itoa(cNvPrID),
},
},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
URI: NameSpaceDrawingMLChart.Value,
Chart: &xlsxChart{
C: NameSpaceDrawingMLChart.Value,
R: SourceRelationship.Value,
RID: "rId" + strconv.Itoa(rID),
},
},
},
}
graphic, _ := xml.Marshal(graphicFrame)
twoCellAnchor.GraphicFrame = string(graphic)
twoCellAnchor.ClientData = &xdrClientData{
FLocksWithSheet: *opts.Locked,
FPrintsWithSheet: *opts.PrintObject,
}
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
f.Drawings.Store(drawingXML, content)
return err
}
// addSheetDrawingChart provides a function to add chart graphic frame for
// chartsheet by given sheet, drawingXML, width, height, relationship index
// and format sets.
func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOptions) error {
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
}
absoluteAnchor := xdrCellAnchor{
EditAs: opts.Positioning,
Pos: &xlsxPoint2D{},
Ext: &xlsxExt{},
}
graphicFrame := xlsxGraphicFrame{
NvGraphicFramePr: xlsxNvGraphicFramePr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
Name: "Chart " + strconv.Itoa(cNvPrID),
},
},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
URI: NameSpaceDrawingMLChart.Value,
Chart: &xlsxChart{
C: NameSpaceDrawingMLChart.Value,
R: SourceRelationship.Value,
RID: "rId" + strconv.Itoa(rID),
},
},
},
}
graphic, _ := xml.Marshal(graphicFrame)
absoluteAnchor.GraphicFrame = string(graphic)
absoluteAnchor.ClientData = &xdrClientData{
FLocksWithSheet: *opts.Locked,
FPrintsWithSheet: *opts.PrintObject,
}
content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor)
f.Drawings.Store(drawingXML, content)
return err
}
// deleteDrawing provides a function to delete chart graphic frame by given by
// given coordinates and graphic type.
func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error {
var (
err error
wsDr *xlsxWsDr
deTwoCellAnchor *decodeTwoCellAnchor
)
xdrCellAnchorFuncs := map[string]func(anchor *xdrCellAnchor) bool{
"Chart": func(anchor *xdrCellAnchor) bool { return anchor.Pic == nil },
"Pic": func(anchor *xdrCellAnchor) bool { return anchor.Pic != nil },
}
decodeTwoCellAnchorFuncs := map[string]func(anchor *decodeTwoCellAnchor) bool{
"Chart": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic == nil },
"Pic": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic != nil },
}
if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
return err
}
for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ {
if err = nil; wsDr.TwoCellAnchor[idx].From != nil && xdrCellAnchorFuncs[drawingType](wsDr.TwoCellAnchor[idx]) {
if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row {
wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...)
idx--
}
}
}
for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ {
deTwoCellAnchor = new(decodeTwoCellAnchor)
if err = f.xmlNewDecoder(strings.NewReader("<decodeTwoCellAnchor>" + wsDr.TwoCellAnchor[idx].GraphicFrame + "</decodeTwoCellAnchor>")).
Decode(deTwoCellAnchor); err != nil && err != io.EOF {
return err
}
if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) {
if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row {
wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...)
idx--
}
}
}
f.Drawings.Store(drawingXML, wsDr)
return err
}