// Copyright 2016 - 2024 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 or /xl/drawings/drawing%d.xml. sheetRelationshipsDrawingXML = strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "/xl/drawings/", "../drawings/") 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: f.drawPlotAreaTitles(opts.Title, ""), 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: f.drawChartLn(&opts.Border), }, PrintSettings: &cPrintSettings{ PageMargins: &cPageMargins{ B: 0.75, L: 0.7, R: 0.7, T: 0.7, Header: 0.3, Footer: 0.3, }, }, } plotAreaFunc := map[ChartType]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, PieOfPie: f.drawPieOfPieChart, BarOfPie: 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: f.genAxID(opts), 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[ChartType]*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: f.genAxID(opts), }, 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: f.genAxID(opts), }, 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 { var splitPos *attrValInt if opts.PlotArea.SecondPlotValues > 0 { splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} } return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("pie"), }, VaryColors: &attrValBool{ Val: opts.VaryColors, }, Ser: f.drawChartSeries(opts), SplitPos: splitPos, 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 { var splitPos *attrValInt if opts.PlotArea.SecondPlotValues > 0 { splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} } return &cPlotArea{ OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("bar"), }, VaryColors: &attrValBool{ Val: opts.VaryColors, }, SplitPos: splitPos, 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: f.genAxID(opts), }, 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: f.genAxID(opts), }, 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(100000000)}, {Val: intPtr(100000001)}, {Val: intPtr(100000005)}, }, }, 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(100000000)}, {Val: intPtr(100000001)}, {Val: intPtr(100000005)}, }, }, 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: f.genAxID(opts), }, ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]}, } if opts.BubbleSize > 0 && opts.BubbleSize <= 300 { plotArea.BubbleChart.BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))} } 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[ChartType]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(k, 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: &attrValString{}, }, } 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[ChartType]*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[ChartType][]*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[ChartType]*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[ChartType]*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[ChartType]*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[ChartType]*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[ChartType]*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[ChartType]*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[ChartType]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok { return nil } fVal := v.Values if v.Sizes != "" { fVal = v.Sizes } return &cVal{ NumRef: &cNumRef{ F: fVal, }, } } // drawCharSeriesBubble3D provides a function to draw the c:bubble3D element // by given format sets. func (f *File) drawCharSeriesBubble3D(opts *Chart) *attrValBool { if _, ok := map[ChartType]bool{Bubble3D: true}[opts.Type]; !ok { return nil } return &attrValBool{Val: boolPtr(true)} } // drawChartNumFmt provides a function to draw the c:numFmt element by given // data labels format sets. func (f *File) drawChartNumFmt(labels ChartNumFmt) *cNumFmt { var numFmt *cNumFmt if labels.CustomNumFmt != "" || labels.SourceLinked { numFmt = &cNumFmt{ FormatCode: labels.CustomNumFmt, SourceLinked: labels.SourceLinked, } } return numFmt } // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. func (f *File) drawChartDLbls(opts *Chart) *cDLbls { return &cDLbls{ NumFmt: f.drawChartNumFmt(opts.PlotArea.NumFmt), 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)}, } } // inSupportedChartDataLabelsPositionType provides a method to check if an // element is present in an array, and return the index of its location, // otherwise return -1. func inSupportedChartDataLabelsPositionType(a []ChartDataLabelPositionType, x ChartDataLabelPositionType) int { for idx, n := range a { if x == n { return idx } } return -1 } // drawChartSeriesDLbls provides a function to draw the c:dLbls element by // given format sets. func (f *File) drawChartSeriesDLbls(i int, opts *Chart) *cDLbls { dLbls := f.drawChartDLbls(opts) chartSeriesDLbls := map[ChartType]*cDLbls{ Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, } if _, ok := chartSeriesDLbls[opts.Type]; ok { return nil } if types, ok := supportedChartDataLabelsPosition[opts.Type]; ok && opts.Series[i].DataLabelPosition != ChartDataLabelsPositionUnset { if inSupportedChartDataLabelsPositionType(types, opts.Series[i].DataLabelPosition) != -1 { dLbls.DLblPos = &attrValString{Val: stringPtr(chartDataLabelsPositionTypes[opts.Series[i].DataLabelPosition])} } } 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(100000000)}, 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"}, MajorTickMark: &attrValString{Val: stringPtr("none")}, MinorTickMark: &attrValString{Val: stringPtr("none")}, Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""), TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.YAxis), CrossAx: &attrValInt{Val: intPtr(100000001)}, 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 numFmt := f.drawChartNumFmt(opts.XAxis.NumFmt); numFmt != nil { axs[0].NumFmt = numFmt } 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)} } if opts.order > 0 && opts.YAxis.Secondary { axs = append(axs, &cAxs{ AxID: &attrValInt{Val: intPtr(opts.XAxis.axID)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, Max: max, Min: min, }, Delete: &attrValBool{Val: boolPtr(true)}, AxPos: &attrValString{Val: stringPtr("b")}, 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(opts.YAxis.axID)}, Auto: &attrValBool{Val: boolPtr(true)}, LblAlgn: &attrValString{Val: stringPtr("ctr")}, LblOffset: &attrValInt{Val: intPtr(100)}, NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, }) } 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(100000001)}, 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])}, Title: f.drawPlotAreaTitles(opts.YAxis.Title, "horz"), NumFmt: &cNumFmt{ FormatCode: chartValAxNumFmtFormatCode[opts.Type], }, 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(100000000)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, }, } if numFmt := f.drawChartNumFmt(opts.YAxis.NumFmt); numFmt != nil { axs[0].NumFmt = numFmt } 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)} } if opts.order > 0 && opts.YAxis.Secondary { axs = append(axs, &cAxs{ AxID: &attrValInt{Val: intPtr(opts.YAxis.axID)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, Max: max, Min: min, }, Delete: &attrValBool{Val: boolPtr(false)}, AxPos: &attrValString{Val: stringPtr("r")}, 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(opts.XAxis.axID)}, Crosses: &attrValString{Val: stringPtr("max")}, CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, }) } 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(100000005)}, 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(100000001)}, }, } } // drawPlotAreaTitles provides a function to draw the c:title element. func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle { if len(runs) == 0 { return nil } title := &cTitle{Tx: cTx{Rich: &cRich{}}, Overlay: &attrValBool{Val: boolPtr(false)}} for _, run := range runs { r := &aR{T: run.Text} if run.Font != nil { r.RPr.B, r.RPr.I = run.Font.Bold, run.Font.Italic if run.Font.Color != "" { r.RPr.SolidFill = &aSolidFill{SrgbClr: &attrValString{Val: stringPtr(run.Font.Color)}} } if run.Font.Size > 0 { r.RPr.Sz = run.Font.Size * 100 } } title.Tx.Rich.P = append(title.Tx.Rich.P, aP{ PPr: &aPPr{DefRPr: aRPr{}}, R: r, EndParaRPr: &aEndParaRPr{Lang: "en-US", AltLang: "en-US"}, }) } if vert == "horz" { title.Tx.Rich.BodyPr = aBodyPr{Rot: -5400000, Vert: vert} } return title } // 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 } // drawChartLn provides a function to draw the a:ln element. func (f *File) drawChartLn(opts *ChartLine) *aLn { ln := &aLn{ W: f.ptToEMUs(opts.Width), Cap: "flat", Cmpd: "sng", Algn: "ctr", } switch opts.Type { case ChartLineSolid: ln.SolidFill = &aSolidFill{ SchemeClr: &aSchemeClr{ Val: "tx1", LumMod: &attrValInt{ Val: intPtr(15000), }, LumOff: &attrValInt{ Val: intPtr(85000), }, }, } return ln case ChartLineNone: ln.NoFill = &attrValString{} return ln default: return nil } } // 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{ NS: NameSpaceDrawingMLSpreadSheet.Value, Xdr: NameSpaceDrawingMLSpreadSheet.Value, A: NameSpaceDrawingML.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.mu.Lock() defer wsDr.mu.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 } width = int(float64(width) * opts.ScaleX) height = int(float64(height) * opts.ScaleY) colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, 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: &aExt{}, } 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 the chart graphic frame and // returns deleted embed relationships ID (for unique picture cell anchor) by // given coordinates and graphic type. func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (string, error) { var ( err error rID string rIDs []string wsDr *xlsxWsDr deTwoCellAnchor *decodeCellAnchor ) 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 }, } decodeCellAnchorFuncs := map[string]func(anchor *decodeCellAnchor) bool{ "Chart": func(anchor *decodeCellAnchor) bool { return anchor.Pic == nil }, "Pic": func(anchor *decodeCellAnchor) bool { return anchor.Pic != nil }, } onAnchorCell := func(c, r int) bool { return c == col && r == row } if wsDr, _, err = f.drawingParser(drawingXML); err != nil { return rID, err } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { if err = nil; wsDr.TwoCellAnchor[idx].From != nil && xdrCellAnchorFuncs[drawingType](wsDr.TwoCellAnchor[idx]) { if onAnchorCell(wsDr.TwoCellAnchor[idx].From.Col, wsDr.TwoCellAnchor[idx].From.Row) { rID, _ = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs) wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) idx-- continue } _, rIDs = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs) } } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeCellAnchor) if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { return rID, err } if err = nil; deTwoCellAnchor.From != nil && decodeCellAnchorFuncs[drawingType](deTwoCellAnchor) { if onAnchorCell(deTwoCellAnchor.From.Col, deTwoCellAnchor.From.Row) { rID, _ = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs) wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) idx-- continue } _, rIDs = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs) } } if inStrSlice(rIDs, rID, true) != -1 { rID = "" } f.Drawings.Store(drawingXML, wsDr) return rID, err } // extractEmbedRID returns embed relationship ID and all relationship ID lists // for giving cell anchor. func extractEmbedRID(pic *xlsxPic, decodePic *decodePic, rIDs []string) (string, []string) { if pic != nil { rIDs = append(rIDs, pic.BlipFill.Blip.Embed) return pic.BlipFill.Blip.Embed, rIDs } if decodePic != nil { rIDs = append(rIDs, decodePic.BlipFill.Blip.Embed) return decodePic.BlipFill.Blip.Embed, rIDs } return "", rIDs } // deleteDrawingRels provides a function to delete relationships in // xl/drawings/_rels/drawings%d.xml.rels by giving drawings relationships path // and relationship ID. func (f *File) deleteDrawingRels(rels, rID string) { drawingRels, _ := f.relsReader(rels) if drawingRels == nil { drawingRels = &xlsxRelationships{} } drawingRels.mu.Lock() defer drawingRels.mu.Unlock() for k, v := range drawingRels.Relationships { if v.ID == rID { drawingRels.Relationships = append(drawingRels.Relationships[:k], drawingRels.Relationships[k+1:]...) } } f.Relationships.Store(rels, drawingRels) } // genAxID provides a function to generate ID for primary and secondary // horizontal or vertical axis. func (f *File) genAxID(opts *Chart) []*attrValInt { opts.XAxis.axID, opts.YAxis.axID = 100000000, 100000001 if opts.order > 0 && opts.YAxis.Secondary { opts.XAxis.axID, opts.YAxis.axID = 100000003, 100000004 } return []*attrValInt{{Val: intPtr(opts.XAxis.axID)}, {Val: intPtr(opts.YAxis.axID)}} }