The reader of the CSV file of a chart
A delimiter, a comma by default, divides the cells of a row. A cell in
double quotes can hold the delimiter, a line break and a quote, which two
quotes give. The first row holds the headings, and each other row holds a
category and one number for each series. table/3 reads a file without
frames. frames/4 reads a file with a column that names the frame of each
row. Each function returns the text of an error for a file that it cannot
read, and Expresso.Element.Chart gives that error to the author.
The cell option gives the function that reads a cell of a value. The
default is number/1, and a waterfall chart also reads the mark of a
total.
For a treemap or a sunburst, the levels option can name the columns that
hold the levels of a category, from the top level down. The category of a
row is then the list of its levels, and an empty cell at the end of the
list ends the category at a higher level.
Summary
Types
The options of a reader: levels, the headings of the columns that hold
the levels of a category, delimiter, the character between two cells,
cell, the function that reads a cell of a value and returns nil for a
cell that is not a value, and needs, the words after the number of values
in the error of a row with a bad cell.
Functions
Read a CSV file with a column that names the frame of each row
Return the heading of the column of the categories, or nil
Join the columns of the levels of each row into one category
Read the number of a cell, or return nil for a cell that is not a number
Divide the text of a chart file into rows of cells
Read a CSV file of categories and series
Types
@type opts() :: [ levels: [String.t()] | nil, delimiter: String.t(), cell: (String.t() -> term() | nil), needs: String.t() ]
The options of a reader: levels, the headings of the columns that hold
the levels of a category, delimiter, the character between two cells,
cell, the function that reads a cell of a value and returns nil for a
cell that is not a value, and needs, the words after the number of values
in the error of a row with a bad cell.
Functions
@spec frames(String.t(), String.t(), String.t(), opts()) :: {:ok, [String.t() | [String.t()]], [Expresso.Element.Frame.t()]} | {:error, String.t()}
Read a CSV file with a column that names the frame of each row
The column with the heading column names the frame. Of the other columns,
the first holds the category, and each other column holds a series. The
frames have the order of their first row, and each frame holds each
category of the first frame one time, in any order. The function puts the
categories of each frame into the order of the first frame.
iex> text = "year,region,Orders\n2024,North,1\n2024,South,2\n2025,South,4\n2025,North,3\n"
iex> {:ok, categories, [_first, second]} = Expresso.Element.Chart.Csv.frames(text, "year", "a.csv")
iex> {categories, second.label, hd(second.elements).values}
{["North", "South"], "2025", [3, 4]}
Return the heading of the column of the categories, or nil
The heading is the first cell of the first row. In a file with a column of
frames, it is the first cell of the other columns. A file with levels joins
the headings of its levels. An empty heading, or a file that the other
functions refuse, returns nil.
iex> Expresso.Element.Chart.Csv.heading("Year,Orders\n2024,1\n", "a.csv")
"Year"
iex> Expresso.Element.Chart.Csv.heading("year,region,Orders\n2024,North,1\n", "a.csv", [], "year")
"region"
iex> Expresso.Element.Chart.Csv.heading(",Orders\n2024,1\n", "a.csv")
nil
@spec levels([{pos_integer(), [String.t()]}], [String.t()] | nil, String.t()) :: {:ok, [{pos_integer(), [term()]}]} | {:error, String.t()}
Join the columns of the levels of each row into one category
The category is the first cell of the row, and the other cells keep their order. Without levels, the function returns the rows as they are.
iex> rows = [{1, ["Division", "Team", "Hours"]}, {2, ["Production", "Press", "62"]}, {3, ["Office", "", "20"]}]
iex> Expresso.Element.Chart.Csv.levels(rows, ["Division", "Team"], "a.csv")
{:ok, [{1, ["Division/Team", "Hours"]}, {2, [["Production", "Press"], "62"]}, {3, [["Office"], "20"]}]}
Read the number of a cell, or return nil for a cell that is not a number
A number can have a comma between each group of three digits, as a spreadsheet writes it.
iex> Expresso.Element.Chart.Csv.number(" 1,250 ")
1250
iex> Expresso.Element.Chart.Csv.number("2.5")
2.5
iex> Expresso.Element.Chart.Csv.number("many")
nil
@spec rows(String.t(), String.t(), opts()) :: {:ok, [{pos_integer(), [String.t()]}]} | {:error, String.t()}
Divide the text of a chart file into rows of cells
Each row has the number of its first line. The function trims the spaces around a cell without quotes, and it keeps the text in quotes as it is. It skips a line with only white space. It removes a byte order mark at the start of the text, as the format "CSV UTF-8" of a spreadsheet writes it, so the first cell keeps its name.
iex> Expresso.Element.Chart.Csv.rows("\uFEFFyear,Orders\n2024,3\n", "a.csv")
{:ok, [{1, ["year", "Orders"]}, {2, ["2024", "3"]}]}
iex> Expresso.Element.Chart.Csv.rows("a; b\n\n\"x\"\"y\";\"1\n2\"\n", "a.csv", delimiter: ";")
{:ok, [{1, ["a", "b"]}, {3, [~s(x"y), "1\n2"]}]}
@spec table(String.t(), String.t(), opts()) :: {:ok, [String.t() | [String.t()]], [Expresso.Element.Series.t()]} | {:error, String.t()}
Read a CSV file of categories and series
The first row holds a heading for the categories, then the name of each series. Each other row holds a category, then one number for each series.
iex> Expresso.Element.Chart.Csv.table("year,Orders\n2024,180\n2025,240\n", "a.csv")
{:ok, ["2024", "2025"], [%Expresso.Element.Series{name: "Orders", values: [180, 240]}]}A cell in quotes can hold the delimiter, and a number in quotes can have a comma between each group of three digits:
iex> text = ~s(Plant,Orders\n"Leeds, Line 4","1,250"\n)
iex> Expresso.Element.Chart.Csv.table(text, "a.csv")
{:ok, ["Leeds, Line 4"], [%Expresso.Element.Series{name: "Orders", values: [1250]}]}