trackplot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ module Trackplot
2
+ module ChartHelper
3
+ def trackplot_chart(data, **options, &block)
4
+ builder = ChartBuilder.new(data, **options)
5
+ capture(builder, &block) if block_given?
6
+ builder.render(self)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ pin "d3", to: "https://cdn.jsdelivr.net/npm/d3@7/+esm", preload: false
2
+ pin "trackplot", to: "trackplot/index.js", preload: true
@@ -0,0 +1,23 @@
1
+ module Trackplot
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ desc "Install Trackplot into your Rails application"
5
+
6
+ def add_import
7
+ js_file = Rails.root.join("app/javascript/application.js")
8
+
9
+ if File.exist?(js_file)
10
+ content = File.read(js_file)
11
+ if content.include?('import "trackplot"')
12
+ say "trackplot import already present in application.js", :yellow
13
+ else
14
+ append_to_file js_file, "\nimport \"trackplot\"\n"
15
+ say "Added trackplot import to application.js", :green
16
+ end
17
+ else
18
+ say "Could not find app/javascript/application.js — please add `import \"trackplot\"` manually", :red
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,119 @@
1
+ require "securerandom"
2
+ require "json"
3
+
4
+ module Trackplot
5
+ class ChartBuilder
6
+ attr_reader :data, :options, :components
7
+
8
+ def initialize(data, **options)
9
+ @data = DataAdapter.normalize(data)
10
+ @options = options
11
+ @components = []
12
+ end
13
+
14
+ def line(data_key, **opts)
15
+ @components << Components::Line.new(data_key, **opts)
16
+ nil
17
+ end
18
+
19
+ def bar(data_key, **opts)
20
+ @components << Components::Bar.new(data_key, **opts)
21
+ nil
22
+ end
23
+
24
+ def area(data_key, **opts)
25
+ @components << Components::Area.new(data_key, **opts)
26
+ nil
27
+ end
28
+
29
+ def pie(data_key, **opts)
30
+ @components << Components::Pie.new(data_key, **opts)
31
+ nil
32
+ end
33
+
34
+ def axis(direction, **opts)
35
+ @components << Components::Axis.new(direction, **opts)
36
+ nil
37
+ end
38
+
39
+ def tooltip(**opts)
40
+ @components << Components::Tooltip.new(**opts)
41
+ nil
42
+ end
43
+
44
+ def legend(**opts)
45
+ @components << Components::Legend.new(**opts)
46
+ nil
47
+ end
48
+
49
+ def grid(**opts)
50
+ @components << Components::Grid.new(**opts)
51
+ nil
52
+ end
53
+
54
+ def scatter(data_key, **opts)
55
+ @components << Components::Scatter.new(data_key, **opts)
56
+ nil
57
+ end
58
+
59
+ def radar(data_key, **opts)
60
+ @components << Components::Radar.new(data_key, **opts)
61
+ nil
62
+ end
63
+
64
+ def horizontal_bar(data_key, **opts)
65
+ @components << Components::HorizontalBar.new(data_key, **opts)
66
+ nil
67
+ end
68
+
69
+ def candlestick(**opts)
70
+ @components << Components::Candlestick.new(**opts)
71
+ nil
72
+ end
73
+
74
+ def funnel(data_key, **opts)
75
+ @components << Components::Funnel.new(data_key, **opts)
76
+ nil
77
+ end
78
+
79
+ def reference_line(**opts)
80
+ @components << Components::ReferenceLine.new(**opts)
81
+ nil
82
+ end
83
+
84
+ def render(view_context)
85
+ chart_id = options[:id] || "trackplot-#{SecureRandom.hex(8)}"
86
+ config = build_config
87
+
88
+ view_context.content_tag(
89
+ "trackplot-chart",
90
+ nil,
91
+ id: chart_id,
92
+ config: config.to_json,
93
+ style: chart_style,
94
+ class: css_classes
95
+ )
96
+ end
97
+
98
+ private
99
+
100
+ def build_config
101
+ {
102
+ data: data,
103
+ components: components.map(&:to_config),
104
+ animate: options.fetch(:animate, true),
105
+ theme: Theme.resolve(options[:theme])
106
+ }
107
+ end
108
+
109
+ def chart_style
110
+ width = options[:width] || "100%"
111
+ height = options[:height] || "400px"
112
+ "display:block;width:#{width};height:#{height};"
113
+ end
114
+
115
+ def css_classes
116
+ ["trackplot-chart", options[:class]].compact.join(" ")
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,24 @@
1
+ module Trackplot
2
+ module Components
3
+ class Area < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "area",
14
+ data_key: data_key,
15
+ color: options[:color],
16
+ curve: options.fetch(:curve, false),
17
+ opacity: options[:opacity] || 0.3,
18
+ stroke_width: options[:stroke_width] || 2,
19
+ stack: options[:stack]
20
+ }.compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ module Trackplot
2
+ module Components
3
+ class Axis < Base
4
+ FORMAT_SYMBOLS = {
5
+ currency: "currency",
6
+ percent: "percent",
7
+ compact: "compact",
8
+ decimal: "decimal",
9
+ integer: "integer"
10
+ }.freeze
11
+
12
+ attr_reader :direction
13
+
14
+ def initialize(direction, **options)
15
+ @direction = direction.to_s
16
+ super(**options)
17
+ end
18
+
19
+ def to_config
20
+ {
21
+ type: "axis",
22
+ direction: direction,
23
+ data_key: options[:data_key],
24
+ label: options[:label],
25
+ format: resolve_format(options[:format]),
26
+ tick_count: options[:tick_count],
27
+ tick_rotation: options[:tick_rotation]
28
+ }.compact
29
+ end
30
+
31
+ private
32
+
33
+ def resolve_format(fmt)
34
+ return nil if fmt.nil?
35
+
36
+ FORMAT_SYMBOLS.fetch(fmt, fmt).to_s
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ module Trackplot
2
+ module Components
3
+ class Bar < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "bar",
14
+ data_key: data_key,
15
+ color: options[:color],
16
+ opacity: options[:opacity],
17
+ radius: options[:radius] || 4,
18
+ stack: options[:stack]
19
+ }.compact
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Trackplot
2
+ module Components
3
+ class Base
4
+ attr_reader :options
5
+
6
+ def initialize(**options)
7
+ @options = options
8
+ end
9
+
10
+ def type
11
+ self.class.name.demodulize.underscore
12
+ end
13
+
14
+ def to_config
15
+ {type: type}.merge(options).compact
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Trackplot
2
+ module Components
3
+ class Candlestick < Base
4
+ def to_config
5
+ {
6
+ type: "candlestick",
7
+ open: options[:open],
8
+ high: options[:high],
9
+ low: options[:low],
10
+ close: options[:close],
11
+ up_color: options[:up_color] || "#10b981",
12
+ down_color: options[:down_color] || "#ef4444"
13
+ }.compact
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Trackplot
2
+ module Components
3
+ class Funnel < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "funnel",
14
+ data_key: data_key,
15
+ label_key: options[:label_key]
16
+ }.compact
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Trackplot
2
+ module Components
3
+ class Grid < Base
4
+ def to_config
5
+ {
6
+ type: "grid",
7
+ horizontal: options.fetch(:horizontal, true),
8
+ vertical: options.fetch(:vertical, false)
9
+ }.compact
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module Trackplot
2
+ module Components
3
+ class HorizontalBar < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "horizontal_bar",
14
+ data_key: data_key,
15
+ color: options[:color],
16
+ opacity: options[:opacity],
17
+ radius: options[:radius] || 4
18
+ }.compact
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module Trackplot
2
+ module Components
3
+ class Legend < Base
4
+ def to_config
5
+ {
6
+ type: "legend",
7
+ position: options[:position] || "bottom",
8
+ clickable: options.fetch(:clickable, true)
9
+ }.compact
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ module Trackplot
2
+ module Components
3
+ class Line < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "line",
14
+ data_key: data_key,
15
+ color: options[:color],
16
+ curve: options.fetch(:curve, false),
17
+ dashed: options.fetch(:dashed, false),
18
+ stroke_width: options[:stroke_width] || 2,
19
+ dot: options.fetch(:dot, true),
20
+ dot_size: options[:dot_size] || 4
21
+ }.compact
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Trackplot
2
+ module Components
3
+ class Pie < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "pie",
14
+ data_key: data_key,
15
+ label_key: options[:label_key],
16
+ donut: options.fetch(:donut, false),
17
+ inner_radius: options[:inner_radius],
18
+ pad_angle: options[:pad_angle] || 0.02
19
+ }.compact
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ module Trackplot
2
+ module Components
3
+ class Radar < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "radar",
14
+ data_key: data_key,
15
+ color: options[:color],
16
+ opacity: options[:opacity] || 0.15,
17
+ stroke_width: options[:stroke_width] || 2,
18
+ dot: options.fetch(:dot, true),
19
+ dot_size: options[:dot_size] || 4
20
+ }.compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Trackplot
2
+ module Components
3
+ class ReferenceLine < Base
4
+ def initialize(**options)
5
+ super
6
+ end
7
+
8
+ def to_config
9
+ direction = options[:y] ? "y" : "x"
10
+ value = options[:y] || options[:x]
11
+
12
+ {
13
+ type: "reference_line",
14
+ direction: direction,
15
+ value: value,
16
+ label: options[:label],
17
+ color: options[:color] || "#ef4444",
18
+ dashed: options.fetch(:dashed, true),
19
+ stroke_width: options[:stroke_width] || 1.5
20
+ }.compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Trackplot
2
+ module Components
3
+ class Scatter < Base
4
+ attr_reader :data_key
5
+
6
+ def initialize(data_key, **options)
7
+ @data_key = data_key
8
+ super(**options)
9
+ end
10
+
11
+ def to_config
12
+ {
13
+ type: "scatter",
14
+ data_key: data_key,
15
+ x_key: options[:x_key],
16
+ color: options[:color],
17
+ dot_size: options[:dot_size] || 5,
18
+ opacity: options[:opacity] || 0.7,
19
+ shape: options[:shape] || "circle"
20
+ }.compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module Trackplot
2
+ module Components
3
+ class Tooltip < Base
4
+ FORMAT_SYMBOLS = {
5
+ currency: "currency",
6
+ percent: "percent",
7
+ compact: "compact",
8
+ decimal: "decimal",
9
+ integer: "integer"
10
+ }.freeze
11
+
12
+ def to_config
13
+ {
14
+ type: "tooltip",
15
+ format: resolve_format(options[:format]),
16
+ label_format: options[:label_format]
17
+ }.compact
18
+ end
19
+
20
+ private
21
+
22
+ def resolve_format(fmt)
23
+ return nil if fmt.nil?
24
+
25
+ FORMAT_SYMBOLS.fetch(fmt, fmt).to_s
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ module Trackplot
2
+ class DataAdapter
3
+ def self.normalize(data)
4
+ records = case data
5
+ when ->(d) { defined?(ActiveRecord::Relation) && d.is_a?(ActiveRecord::Relation) }
6
+ data.map { |record| to_string_hash(record.attributes) }
7
+ when Array
8
+ data.map { |item| coerce_record(item) }
9
+ when Hash
10
+ [data.transform_keys(&:to_s)]
11
+ else
12
+ Array(data).map { |item| coerce_record(item) }
13
+ end
14
+
15
+ records.map { |r| r.transform_keys(&:to_s) }
16
+ end
17
+
18
+ def self.coerce_record(item)
19
+ case item
20
+ when Hash
21
+ item
22
+ when ->(i) { i.respond_to?(:attributes) }
23
+ item.attributes
24
+ when ->(i) { i.respond_to?(:to_h) }
25
+ item.to_h
26
+ else
27
+ raise ArgumentError, "Trackplot: cannot convert #{item.class} to chart data. Expected Hash, ActiveRecord, or an object responding to #to_h."
28
+ end
29
+ end
30
+
31
+ private_class_method :coerce_record
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module Trackplot
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Trackplot
4
+
5
+ initializer "trackplot.helpers" do
6
+ ActiveSupport.on_load(:action_view) do
7
+ include Trackplot::ChartHelper
8
+ end
9
+ end
10
+
11
+ initializer "trackplot.assets" do |app|
12
+ app.config.assets.paths << root.join("app/assets/javascripts") if app.config.respond_to?(:assets)
13
+ end
14
+
15
+ initializer "trackplot.importmap", before: "importmap" do |app|
16
+ if app.config.respond_to?(:importmap)
17
+ app.config.importmap.paths << root.join("config/importmap.rb")
18
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ module Trackplot
2
+ class Theme
3
+ PRESETS = {
4
+ default: {
5
+ colors: ["#6366f1", "#06b6d4", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#14b8a6"],
6
+ background: "transparent",
7
+ text_color: "#374151",
8
+ axis_color: "#d1d5db",
9
+ grid_color: "#e5e7eb",
10
+ tooltip_bg: "rgba(255, 255, 255, 0.96)",
11
+ tooltip_text: "#111827",
12
+ tooltip_border: "#e5e7eb",
13
+ font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
14
+ },
15
+ dark: {
16
+ colors: ["#818cf8", "#22d3ee", "#34d399", "#fbbf24", "#f87171", "#a78bfa", "#f472b6", "#2dd4bf"],
17
+ background: "#1e1e2e",
18
+ text_color: "#e2e8f0",
19
+ axis_color: "#4a5568",
20
+ grid_color: "#2d3748",
21
+ tooltip_bg: "rgba(30, 30, 46, 0.96)",
22
+ tooltip_text: "#f1f5f9",
23
+ tooltip_border: "#4a5568",
24
+ font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
25
+ },
26
+ vibrant: {
27
+ colors: ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff", "#ff922b", "#cc5de8", "#20c997", "#f06595"],
28
+ background: "transparent",
29
+ text_color: "#2d3436",
30
+ axis_color: "#b2bec3",
31
+ grid_color: "#dfe6e9",
32
+ tooltip_bg: "rgba(255, 255, 255, 0.96)",
33
+ tooltip_text: "#2d3436",
34
+ tooltip_border: "#dfe6e9",
35
+ font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
36
+ },
37
+ minimal: {
38
+ colors: ["#475569", "#94a3b8", "#334155", "#64748b", "#1e293b", "#78716c", "#57534e", "#a8a29e"],
39
+ background: "transparent",
40
+ text_color: "#64748b",
41
+ axis_color: "#e2e8f0",
42
+ grid_color: "#f1f5f9",
43
+ tooltip_bg: "rgba(255, 255, 255, 0.96)",
44
+ tooltip_text: "#334155",
45
+ tooltip_border: "#e2e8f0",
46
+ font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
47
+ }
48
+ }.freeze
49
+
50
+ def self.resolve(option)
51
+ case option
52
+ when nil then PRESETS[:default]
53
+ when Symbol then PRESETS.fetch(option) { raise ArgumentError, "Unknown theme: #{option}. Available: #{PRESETS.keys.join(", ")}" }
54
+ when Hash then PRESETS[:default].merge(option.transform_keys(&:to_sym))
55
+ else raise ArgumentError, "Theme must be a Symbol, Hash, or nil"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module Trackplot
2
+ VERSION = "0.1.0"
3
+ end
data/lib/trackplot.rb ADDED
@@ -0,0 +1,26 @@
1
+ require_relative "trackplot/version"
2
+ require_relative "trackplot/engine" if defined?(Rails::Engine)
3
+
4
+ module Trackplot
5
+ autoload :ChartBuilder, "trackplot/chart_builder"
6
+ autoload :DataAdapter, "trackplot/data_adapter"
7
+ autoload :Theme, "trackplot/theme"
8
+
9
+ module Components
10
+ autoload :Base, "trackplot/components/base"
11
+ autoload :Line, "trackplot/components/line"
12
+ autoload :Bar, "trackplot/components/bar"
13
+ autoload :Area, "trackplot/components/area"
14
+ autoload :Pie, "trackplot/components/pie"
15
+ autoload :Axis, "trackplot/components/axis"
16
+ autoload :Tooltip, "trackplot/components/tooltip"
17
+ autoload :Legend, "trackplot/components/legend"
18
+ autoload :Grid, "trackplot/components/grid"
19
+ autoload :Scatter, "trackplot/components/scatter"
20
+ autoload :Radar, "trackplot/components/radar"
21
+ autoload :HorizontalBar, "trackplot/components/horizontal_bar"
22
+ autoload :Candlestick, "trackplot/components/candlestick"
23
+ autoload :Funnel, "trackplot/components/funnel"
24
+ autoload :ReferenceLine, "trackplot/components/reference_line"
25
+ end
26
+ end