Skip to content

Commit ae22f1e

Browse files
committed
wip
1 parent 5149bb5 commit ae22f1e

File tree

9 files changed

+939
-473
lines changed

9 files changed

+939
-473
lines changed

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension)
1616
project(${TARGET_NAME})
1717
include_directories(src/include)
1818

19-
set(EXTENSION_SOURCES src/textplot_extension.cpp)
19+
set(EXTENSION_SOURCES
20+
src/textplot_extension.cpp
21+
src/textplot_bar.cpp
22+
src/textplot_density.cpp
23+
src/textplot_sparkline.cpp
24+
)
2025

2126
build_static_extension(${TARGET_NAME} ${EXTENSION_SOURCES})
2227
build_loadable_extension(${TARGET_NAME} " " ${EXTENSION_SOURCES})

src/include/textplot_bar.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#pragma once
2+
3+
#include "duckdb.hpp"
4+
#include "duckdb/function/scalar_function.hpp"
5+
#include "duckdb/common/exception.hpp"
6+
#include <unordered_map>
7+
#include <vector>
8+
9+
namespace duckdb {
10+
11+
// Function declarations
12+
unique_ptr<FunctionData> TextplotBarBind(ClientContext &context, ScalarFunction &bound_function,
13+
vector<unique_ptr<Expression>> &arguments);
14+
15+
void TextplotBar(DataChunk &args, ExpressionState &state, Vector &result);
16+
17+
} // namespace duckdb

src/include/textplot_density.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#pragma once
2+
3+
#include "duckdb.hpp"
4+
#include "duckdb/function/scalar_function.hpp"
5+
#include "duckdb/common/exception.hpp"
6+
#include <unordered_map>
7+
#include <vector>
8+
9+
namespace duckdb {
10+
11+
// Function declarations
12+
unique_ptr<FunctionData> TextplotDensityBind(ClientContext &context, ScalarFunction &bound_function,
13+
vector<unique_ptr<Expression>> &arguments);
14+
15+
void TextplotDensity(DataChunk &args, ExpressionState &state, Vector &result);
16+
17+
} // namespace duckdb

src/include/textplot_sparkline.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#pragma once
2+
3+
#include "duckdb.hpp"
4+
#include "duckdb/function/scalar_function.hpp"
5+
#include "duckdb/common/exception.hpp"
6+
#include <unordered_map>
7+
#include <vector>
8+
9+
namespace duckdb {
10+
11+
// Function declarations
12+
unique_ptr<FunctionData> TextplotSparklineBind(ClientContext &context, ScalarFunction &bound_function,
13+
vector<unique_ptr<Expression>> &arguments);
14+
15+
void TextplotSparkline(DataChunk &args, ExpressionState &state, Vector &result);
16+
17+
} // namespace duckdb

src/textplot_bar.cpp

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#include "textplot_bar.hpp"
2+
#include "duckdb/common/string_util.hpp"
3+
#include "duckdb/main/extension_util.hpp"
4+
#include "duckdb/function/scalar_function.hpp"
5+
#include "duckdb/common/vector_operations/unary_executor.hpp"
6+
#include "duckdb/planner/expression/bound_function_expression.hpp"
7+
#include "duckdb/execution/expression_executor.hpp"
8+
#include <algorithm>
9+
10+
namespace duckdb {
11+
12+
// Bar chart emoji lookup tables
13+
const std::unordered_map<std::string, std::string> emoji_squares = {{"red", "🟥"}, {"orange", "🟧"}, {"yellow", "🟨"},
14+
{"green", "🟩"}, {"blue", "🟦"}, {"purple", "🟪"},
15+
{"brown", "🟫"}, {"black", ""}, {"white", ""}};
16+
17+
const std::unordered_map<std::string, std::string> emoji_circles = {{"red", "🔴"}, {"orange", "🟠"}, {"yellow", "🟡"},
18+
{"green", "🟢"}, {"blue", "🔵"}, {"purple", "🟣"},
19+
{"brown", "🟤"}, {"black", ""}, {"white", ""}};
20+
21+
const std::unordered_map<std::string, std::string> emoji_hearts = {{"red", "❤️"}, {"orange", "🧡"}, {"yellow", "💛"},
22+
{"green", "💚"}, {"blue", "💙"}, {"purple", "💜"},
23+
{"brown", "🤎"}, {"black", "🖤"}, {"white", "🤍"}};
24+
25+
// Bar chart bind data structure
26+
struct TextplotBarBindData : public FunctionData {
27+
double min = 0;
28+
double max = 1.0;
29+
int64_t width = 10;
30+
string on = "_";
31+
string off = "*";
32+
bool filled = true;
33+
vector<std::pair<double, string>> thresholds;
34+
string char_shape = "square";
35+
string on_color = "red";
36+
string off_color = "white";
37+
38+
TextplotBarBindData(double min_p, double max_p, int64_t width_p, string on_p, string off_p, bool filled_p,
39+
vector<std::pair<double, string>> thresholds_p, string shape_p, string on_color_p,
40+
string off_color_p)
41+
: min(min_p), max(max_p), width(width_p), on(std::move(on_p)), off(std::move(off_p)), filled(filled_p),
42+
thresholds(std::move(thresholds_p)), char_shape(std::move(shape_p)), on_color(std::move(on_color_p)),
43+
off_color(std::move(off_color_p)) {
44+
}
45+
46+
string get_character(double value, bool is_on) const {
47+
if (!is_on) {
48+
if (!off.empty()) {
49+
return off;
50+
}
51+
return get_char(off_color, "white", char_shape);
52+
} else {
53+
if (!on.empty()) {
54+
return on;
55+
}
56+
return get_char(get_threshold_color(value, on_color), "red", char_shape);
57+
}
58+
}
59+
unique_ptr<FunctionData> Copy() const override;
60+
bool Equals(const FunctionData &other_p) const override;
61+
62+
private:
63+
string get_char(const string &color, const string &default_color, const string &shape) const {
64+
const std::unordered_map<std::string, std::string> *lookup_map = nullptr;
65+
if (shape == "square") {
66+
lookup_map = &emoji_squares;
67+
} else if (shape == "circle") {
68+
lookup_map = &emoji_circles;
69+
} else if (shape == "heart") {
70+
lookup_map = &emoji_hearts;
71+
} else {
72+
throw BinderException("tp_bar: 'shape' argument must be one of 'square', 'circle', or 'heart'");
73+
}
74+
75+
if (!color.empty()) {
76+
if (const auto it = lookup_map->find(color); it != lookup_map->end()) {
77+
return it->second;
78+
} else {
79+
throw BinderException(StringUtil::Format("tp_bar: Unknown color value '%s'", color));
80+
}
81+
} else {
82+
return lookup_map->at(default_color);
83+
}
84+
}
85+
std::string get_threshold_color(double n, const string &default_color) const {
86+
if (thresholds.empty()) {
87+
return default_color;
88+
}
89+
for (const auto &t : thresholds) {
90+
if (n >= t.first) {
91+
return t.second;
92+
}
93+
}
94+
return thresholds.back().second;
95+
}
96+
};
97+
98+
unique_ptr<FunctionData> TextplotBarBindData::Copy() const {
99+
return make_uniq<TextplotBarBindData>(min, max, width, on, off, filled, thresholds, char_shape, on_color,
100+
off_color);
101+
}
102+
103+
bool TextplotBarBindData::Equals(const FunctionData &other_p) const {
104+
return true;
105+
}
106+
107+
unique_ptr<FunctionData> TextplotBarBind(ClientContext &context, ScalarFunction &bound_function,
108+
vector<unique_ptr<Expression>> &arguments) {
109+
if (arguments.empty()) {
110+
throw BinderException("tp_bar takes at least one argument");
111+
}
112+
113+
if (!arguments[0]->return_type.IsNumeric()) {
114+
throw InvalidTypeException("tp_bar first argument must be numeric");
115+
}
116+
117+
// Optional arguments
118+
double min = 0;
119+
double max = 1.0;
120+
int64_t width = 10;
121+
string on = "";
122+
string off = "";
123+
string on_color = "";
124+
string off_color = "";
125+
string shape = "";
126+
bool filled = true;
127+
vector<std::pair<double, string>> thresholds;
128+
129+
for (idx_t i = 1; i < arguments.size(); i++) {
130+
const auto &arg = arguments[i];
131+
if (arg->HasParameter()) {
132+
throw ParameterNotResolvedException();
133+
}
134+
if (!arg->IsFoldable()) {
135+
throw BinderException("tp_bar: arguments must be constant");
136+
}
137+
const auto &alias = arg->GetAlias();
138+
if (alias == "min") {
139+
if (!arg->return_type.IsNumeric()) {
140+
throw BinderException("tp_bar: 'min' argument must be numeric");
141+
}
142+
const auto eval_result = ExpressionExecutor::EvaluateScalar(context, *arg);
143+
min = eval_result.CastAs(context, LogicalType::DOUBLE).GetValue<double>();
144+
} else if (alias == "max") {
145+
if (!arg->return_type.IsNumeric()) {
146+
throw BinderException("tp_bar: 'max' argument must be numeric");
147+
}
148+
const auto eval_result = ExpressionExecutor::EvaluateScalar(context, *arg);
149+
max = eval_result.CastAs(context, LogicalType::DOUBLE).GetValue<double>();
150+
} else if (alias == "thresholds") {
151+
if (arg->return_type.InternalType() != PhysicalType::LIST) {
152+
throw BinderException(StringUtil::Format(
153+
"tp_bar: 'thresholds' argument must be a list of structs it is %s", arg->return_type.ToString()));
154+
}
155+
156+
const auto list_children = ListValue::GetChildren(ExpressionExecutor::EvaluateScalar(context, *arg));
157+
for (const auto &list_item : list_children) {
158+
// These should also be lists.
159+
if (list_item.type().InternalType() != PhysicalType::STRUCT) {
160+
throw BinderException(
161+
StringUtil::Format("tp_bar: 'thresholds' child must be a struct it is %s value is %s",
162+
list_item.type().ToString(), list_item.ToString()));
163+
}
164+
// Here you can extract the fields from the struct if needed.
165+
const auto struct_fields = StructValue::GetChildren(list_item);
166+
if (struct_fields.size() != 2) {
167+
throw BinderException(StringUtil::Format(
168+
"tp_bar: 'thresholds' child struct must have 2 fields it has %d", struct_fields.size()));
169+
}
170+
if (!struct_fields[0].type().IsNumeric()) {
171+
throw BinderException(StringUtil::Format(
172+
"tp_bar: 'thresholds' child struct field 'threshold' must be numeric it is %s",
173+
struct_fields[0].type().ToString()));
174+
}
175+
const double threshold = struct_fields[0].CastAs(context, LogicalType::DOUBLE).GetValue<double>();
176+
const string color = struct_fields[1].CastAs(context, LogicalType::VARCHAR).GetValue<string>();
177+
thresholds.emplace_back(threshold, color);
178+
}
179+
std::sort(thresholds.begin(), thresholds.end(), [](const auto &a, const auto &b) {
180+
return a.first > b.first; // descending by .first
181+
});
182+
} else if (alias == "width") {
183+
if (!arg->return_type.IsIntegral()) {
184+
throw BinderException("tp_bar: 'width' argument must be an integer");
185+
}
186+
const auto eval_result = ExpressionExecutor::EvaluateScalar(context, *arg);
187+
width = eval_result.CastAs(context, LogicalType::UBIGINT).GetValue<uint64_t>();
188+
} else if (alias == "filled") {
189+
const auto eval_result = ExpressionExecutor::EvaluateScalar(context, *arg);
190+
filled = eval_result.CastAs(context, LogicalType::BOOLEAN).GetValue<bool>();
191+
} else if (alias == "on") {
192+
if (arg->return_type.id() != LogicalTypeId::VARCHAR) {
193+
throw BinderException("tp_bar: 'on' argument must be a VARCHAR");
194+
}
195+
on = StringValue::Get(ExpressionExecutor::EvaluateScalar(context, *arg));
196+
} else if (alias == "off") {
197+
if (arg->return_type.id() != LogicalTypeId::VARCHAR) {
198+
throw BinderException("tp_bar: 'off' argument must be a VARCHAR");
199+
}
200+
off = StringValue::Get(ExpressionExecutor::EvaluateScalar(context, *arg));
201+
} else if (alias == "off_color") {
202+
if (arg->return_type.id() != LogicalTypeId::VARCHAR) {
203+
throw BinderException("tp_bar: 'off_color' argument must be a VARCHAR");
204+
}
205+
off_color = StringValue::Get(ExpressionExecutor::EvaluateScalar(context, *arg));
206+
} else if (alias == "on_color") {
207+
if (arg->return_type.id() != LogicalTypeId::VARCHAR) {
208+
throw BinderException("tp_bar: 'on_color' argument must be a VARCHAR");
209+
}
210+
on_color = StringValue::Get(ExpressionExecutor::EvaluateScalar(context, *arg));
211+
} else if (alias == "shape") {
212+
if (arg->return_type.id() != LogicalTypeId::VARCHAR) {
213+
throw BinderException("tp_bar: 'shape' argument must be a VARCHAR");
214+
}
215+
shape = StringValue::Get(ExpressionExecutor::EvaluateScalar(context, *arg));
216+
} else {
217+
throw BinderException(StringUtil::Format("tp_bar: Unknown argument '%s'", alias));
218+
}
219+
}
220+
221+
if (shape.empty()) {
222+
shape = "square";
223+
} else {
224+
if (shape != "square" && shape != "circle" && shape != "heart") {
225+
throw BinderException("tp_bar: 'shape' argument must be one of 'square', 'circle', or 'heart'");
226+
}
227+
}
228+
229+
return make_uniq<TextplotBarBindData>(min, max, width, on, off, filled, thresholds, shape, on_color, off_color);
230+
}
231+
232+
void TextplotBar(DataChunk &args, ExpressionState &state, Vector &result) {
233+
auto &value_vector = args.data[0];
234+
const auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
235+
const auto &bind_data = func_expr.bind_info->Cast<TextplotBarBindData>();
236+
237+
UnaryExecutor::Execute<double, string_t>(value_vector, result, args.size(), [&](double value) {
238+
const auto proportion = std::clamp((value - bind_data.min) / (bind_data.max - bind_data.min), 0.0, 1.0);
239+
const auto filled_blocks = static_cast<int64_t>(std::round(bind_data.width * proportion));
240+
241+
string bar;
242+
bar.reserve(bind_data.width * 4); // Reserve space for potential multi-byte characters
243+
for (int64_t i = 0; i < bind_data.width; i++) {
244+
if (bind_data.filled) {
245+
// Fill all blocks up to the proportion
246+
bar += bind_data.get_character(value, i < filled_blocks);
247+
} else {
248+
// Only fill the transition point
249+
bar += bind_data.get_character(value, i == filled_blocks - 1 && filled_blocks > 0);
250+
}
251+
}
252+
return StringVector::AddString(result, bar);
253+
});
254+
}
255+
256+
} // namespace duckdb

0 commit comments

Comments
 (0)