Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

row/col outline level (grouping) support #74

Merged
merged 5 commits into from
Feb 28, 2019
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
10 changes: 10 additions & 0 deletions example.exs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ sheet4
sheet5 = %Sheet{name: "No gridlines shown", show_grid_lines: false}
|> Sheet.set_at(0, 0, "Just this cell")

# Rows/columns can be grouped.
sheet6 = %Sheet{
name: "Row and Column Groups",
rows: 1..100 |> Enum.chunk(10),
group_rows: [{2..3, collapsed: true}, 6..7], # collapse and hide rows 2 to 3
group_cols: [2..9, 2..5] # nest
}
|> Sheet.group_cols("C", "D") # nest further

Workbook.append_sheet(workbook, sheet4)
|> Workbook.append_sheet(sheet5)
|> Workbook.append_sheet(sheet6)
|> Elixlsx.write_to("example.xlsx")
35 changes: 34 additions & 1 deletion lib/elixlsx/sheet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ defmodule Elixlsx.Sheet do
The property list describes formatting options for that
cell. See Font.from_props/1 for a list of options.
"""
defstruct name: "", rows: [], col_widths: %{}, row_heights: %{}, merge_cells: [], pane_freeze: nil, show_grid_lines: true
defstruct name: "", rows: [], col_widths: %{}, row_heights: %{}, group_cols: [], group_rows: [], merge_cells: [], pane_freeze: nil, show_grid_lines: true
@type t :: %Sheet {
name: String.t,
rows: list(list(any())),
col_widths: %{pos_integer => number},
row_heights: %{pos_integer => number},
group_cols: list(rowcol_group),
group_rows: list(rowcol_group),
merge_cells: [],
pane_freeze: {number, number} | nil,
show_grid_lines: boolean()
}
@type rowcol_group :: Range.t | {Range.t, opts :: keyword}

@doc ~S"""
Create a sheet with a sheet name.
Expand Down Expand Up @@ -140,6 +143,36 @@ defmodule Elixlsx.Sheet do
&(Map.put &1, row_idx, height)
end

@spec group_cols(Sheet.t, String.t, String.t) :: Sheet.t
@doc ~S"""
Group given column range. (i.e. increase outline level by one)
Column is indexed by name ("A", ...)

## Options

- `collapsed`: if true, collapse this group.
"""
def group_cols(sheet, first_col, last_col, opts \\ []) do
col_range = Range.new(Util.decode_col(first_col), Util.decode_col(last_col))
new_group = if opts === [], do: col_range, else: {col_range, opts}
update_in(sheet.group_cols, fn groups -> groups ++ [new_group] end)
end

@spec group_rows(Sheet.t, pos_integer, pos_integer) :: Sheet.t
@doc ~S"""
Group given row range. (i.e. increase outline level by one)
Row is indexed starting from 1.

## Options

- `collapsed`: if true, collapse this group.
"""
def group_rows(sheet, first_row_idx, last_row_idx, opts \\ []) do
row_range = Range.new(first_row_idx, last_row_idx)
new_group = if opts === [], do: row_range, else: {row_range, opts}
update_in(sheet.group_rows, fn groups -> groups ++ [new_group] end)
end

@spec set_pane_freeze(Sheet.t, number, number) :: Sheet.t
@doc ~S"""
Set the pane freeze at the given row and column. Row and column are indexed starting from 1.
Expand Down
127 changes: 112 additions & 15 deletions lib/elixlsx/xml_templates.ex
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,23 @@ defmodule Elixlsx.XMLTemplates do
"""
end

defp xl_sheet_rows(data, row_heights, wci) do
Enum.zip(data, 1 .. length data) |>
Enum.map_join(fn {row, rowidx} ->
defp xl_sheet_rows(data, row_heights, grouping_info, wci) do
rows =
Enum.zip(data, 1 .. length data) |>
Enum.map_join(fn {row, rowidx} ->
"""
<row r="#{rowidx}" #{get_row_height_attr(row_heights, rowidx)}>
<row r="#{rowidx}" #{get_row_height_attr(row_heights, rowidx)}#{get_row_grouping_attr(grouping_info, rowidx)}>
#{xl_sheet_cols(row, rowidx, wci)}
</row>
""" end)

if (length(data) + 1) in grouping_info.collapsed_idxs do
rows <> """
<row r="#{length(data) + 1}" collapsed="1"></row>
"""
else
rows
end
end

defp get_row_height_attr(row_heights, rowidx) do
Expand All @@ -272,17 +281,99 @@ defmodule Elixlsx.XMLTemplates do
end
end

defp make_col_width({k, v}) do
'<col min="#{k}" max="#{k}" width="#{v}" customWidth="1" />'
defp get_row_grouping_attr(gr_info, rowidx) do
outline_level = Map.get(gr_info.outline_lvs, rowidx)
(if outline_level, do: " outlineLevel=\"#{outline_level}\"", else: "")
<>
(if rowidx in gr_info.hidden_idxs, do: " hidden=\"1\"", else: "")
<>
(if rowidx in gr_info.collapsed_idxs, do: " collapsed=\"1\"", else: "")
end

@typep grouping_info :: %{
outline_lvs: %{optional(idx :: pos_integer) => lv :: pos_integer},
hidden_idxs: MapSet.t(pos_integer),
collapsed_idxs: MapSet.t(pos_integer)
}
@spec get_grouping_info([Sheet.rowcol_group]) :: grouping_info
defp get_grouping_info(groups) do
ranges =
Enum.map(groups, fn
{%Range{} = range, _opts} -> range
%Range{} = range -> range
end)

collapsed_ranges =
groups
|> Enum.filter(fn
{%Range{} = _range, opts} -> opts[:collapsed]
%Range{} = _range -> false
end)
|> Enum.map(fn {range, _opts} -> range end)

# see ECMA Office Open XML Part1, 18.3.1.73 Row -> attributes -> collapsed for examples
%{
outline_lvs:
ranges
|> Stream.concat()
|> Enum.group_by(& &1)
|> Map.new(fn {k, v} -> {k, length(v)} end),
hidden_idxs:
collapsed_ranges |> Stream.concat() |> MapSet.new(),
collapsed_idxs:
collapsed_ranges |> Enum.map(& &1.last + 1) |> MapSet.new()
}
end

defp make_col({k, width, outline_level, hidden, collapsed}) do
width_attr =
if width, do: " width=\"#{width}\" customWidth=\"1\"", else: ""
hidden_attr = if hidden, do: " hidden=\"1\"", else: ""
outline_level_attr =
if outline_level, do: " outlineLevel=\"#{outline_level}\"", else: ""
collapsed_attr =
if collapsed, do: " collapsed=\"1\"", else: ""

'<col min="#{k}" max="#{k}"#{width_attr}#{hidden_attr}#{outline_level_attr}#{collapsed_attr} />'
end

defp make_cols(sheet) do
grouping_info = get_grouping_info(sheet.group_cols)
col_indices =
Stream.concat([
Map.keys(sheet.col_widths),
Map.keys(grouping_info.outline_lvs),
grouping_info.hidden_idxs,
grouping_info.collapsed_idxs
])
|> Enum.sort()
|> Enum.dedup()

unless Enum.empty?(col_indices) do
cols =
col_indices
|> Stream.map(&({
&1,
Map.get(sheet.col_widths, &1),
Map.get(grouping_info.outline_lvs, &1),
&1 in grouping_info.hidden_idxs,
&1 in grouping_info.collapsed_idxs
}))
|> Enum.map_join(&make_col/1)

"<cols>#{cols}</cols>"
else
""
end
end

defp make_col_widths(sheet) do
if Kernel.map_size(sheet.col_widths) != 0 do
cols = Map.to_list(sheet.col_widths)
|> Enum.sort
|> Enum.map_join(&make_col_width/1)
defp make_max_outline_level_row(row_outline_levels) do
unless row_outline_levels === %{} do
max_outline_level_row =
Map.values(row_outline_levels)
|> Enum.max()

"<cols>#{cols}</cols>"
" outlineLevelRow=\"#{max_outline_level_row}\""
else
""
end
Expand All @@ -293,6 +384,8 @@ defmodule Elixlsx.XMLTemplates do
Returns the XML content for single sheet.
"""
def make_sheet(sheet, wci) do
grouping_info = get_grouping_info(sheet.group_rows)

~S"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
Expand All @@ -311,14 +404,18 @@ defmodule Elixlsx.XMLTemplates do
"""
</sheetView>
</sheetViews>
<sheetFormatPr defaultRowHeight="12.8"/>
<sheetFormatPr defaultRowHeight="12.8"
"""
<> make_max_outline_level_row(grouping_info.outline_lvs) <>
"""
/>
"""
<> make_col_widths(sheet) <>
<> make_cols(sheet) <>
"""
<sheetData>
"""
<>
xl_sheet_rows(sheet.rows, sheet.row_heights, wci)
xl_sheet_rows(sheet.rows, sheet.row_heights, grouping_info, wci)
<>
~S"""
</sheetData>
Expand Down