From f19fc6f41e089ffa69ef13ad43ae8cb0489337ac Mon Sep 17 00:00:00 2001 From: damir Date: Sat, 6 Jan 2024 22:23:14 +0100 Subject: [PATCH 1/4] fix input for bull/bear color selection; make configurable zoom level and colors. --- lib/chart/gallery/ohlc_candle.sample | 18 +-- lib/chart/ohlc.ex | 189 ++++++++++++++++----------- mix.exs | 1 + mix.lock | 1 + 4 files changed, 127 insertions(+), 82 deletions(-) diff --git a/lib/chart/gallery/ohlc_candle.sample b/lib/chart/gallery/ohlc_candle.sample index 5e72247..22b74fa 100644 --- a/lib/chart/gallery/ohlc_candle.sample +++ b/lib/chart/gallery/ohlc_candle.sample @@ -1,15 +1,15 @@ data = [ - [~N[2023-12-28 00:00:00], "AAPL", 34049900, 193.58, 194.14, 194.66, 193.17], - [~N[2023-12-27 00:00:00], "AAPL", 48087680, 193.15, 192.49, 193.50, 191.09], - [~N[2023-12-26 00:00:00], "AAPL", 28919310, 193.05, 193.61, 193.89, 192.83], - [~N[2023-12-25 00:00:00], "AAPL", 37149570, 193.60, 195.18, 195.41, 192.97], - [~N[2023-12-24 00:00:00], "AAPL", 46482550, 194.68, 196.10, 197.08, 193.50], - [~N[2023-12-23 00:00:00], "AAPL", 52242820, 194.83, 196.90, 197.68, 194.83], - [~N[2023-12-22 00:00:00], "AAPL", 40714050, 196.94, 196.16, 196.95, 195.89], - [~N[2023-12-21 00:00:00], "AAPL", 55751860, 195.89, 196.09, 196.63, 194.39] + [~N[2023-12-28 00:00:00], "AAPL", 34049900, 194.14, 194.66, 193.17, 193.58], + [~N[2023-12-27 00:00:00], "AAPL", 48087680, 192.49, 193.50, 191.09, 193.15], + [~N[2023-12-26 00:00:00], "AAPL", 28919310, 193.61, 193.89, 192.83, 193.05], + [~N[2023-12-25 00:00:00], "AAPL", 37149570, 195.18, 195.41, 192.97, 193.60], + [~N[2023-12-24 00:00:00], "AAPL", 46482550, 196.10, 197.08, 193.50, 194.68], + [~N[2023-12-23 00:00:00], "AAPL", 52242820, 196.90, 197.68, 194.83, 194.83], + [~N[2023-12-22 00:00:00], "AAPL", 40714050, 196.16, 196.95, 195.89, 196.94], + [~N[2023-12-21 00:00:00], "AAPL", 55751860, 196.09, 196.63, 194.39, 195.89] ] -test_data = Dataset.new(data, ["Date", "Ticker", "Volume", "Close", "Open", "High", "Low"]) +test_data = Dataset.new(data, ["Date", "Ticker", "Volume", "Open", "High", "Low", "Close"]) options = [ mapping: %{datetime: "Date", open: "Open", high: "High", low: "Low", close: "Close"}, diff --git a/lib/chart/ohlc.ex b/lib/chart/ohlc.ex index 5fff1a1..33a5fa2 100644 --- a/lib/chart/ohlc.ex +++ b/lib/chart/ohlc.ex @@ -27,7 +27,12 @@ defmodule Contex.OHLC do generated to handle the extents of the data. """ + # todo: + # - invert row order and update docs to reflect it (i.e. from past to present not the other way around) + # - refactor :tick into :bar + import Contex.SVG + import Extructure alias __MODULE__ alias Contex.{Scale, ContinuousLinearScale, TimeScale} @@ -44,6 +49,23 @@ defmodule Contex.OHLC do transforms: %{} ] + @type t() :: %__MODULE__{} + @typep row() :: list() + @typep rendered_row() :: list() + @typep color() :: <<_::24>> + + @typep y_vals() :: + %{ + open: number(), + high: number(), + low: number(), + close: number() + } + + @green "00AA00" + @red "AA0000" + @grey "444444" + @required_mappings [ datetime: :exactly_one, open: :exactly_one, @@ -60,7 +82,11 @@ defmodule Contex.OHLC do custom_x_formatter: nil, custom_y_formatter: nil, width: 100, - height: 100 + height: 100, + zoom: 3, + bull_color: @green, + bear_color: @red, + shadow_color: @grey ] @default_plot_options %{ @@ -69,7 +95,16 @@ defmodule Contex.OHLC do legend_setting: :legend_none } - @type t() :: %__MODULE__{} + @zoom_levels [ + [body_width: 0, spacing: 0], + [body_width: 0, spacing: 1], + [body_width: 1, spacing: 1], + [body_width: 3, spacing: 3], + [body_width: 9, spacing: 5], + [body_width: 23, spacing: 7] + ] + |> Stream.with_index() + |> Map.new(fn {k, v} -> {v, Map.new(k)} end) @doc """ Create a new `OHLC` struct from Dataset. @@ -82,17 +117,17 @@ defmodule Contex.OHLC do An example: data = [ - [~N[2023-12-28 00:00:00], "AAPL", 34049900, 193.58, 194.14, 194.66, 193.17], - [~N[2023-12-27 00:00:00], "AAPL", 48087680, 193.15, 192.49, 193.50, 191.09], - [~N[2023-12-26 00:00:00], "AAPL", 28919310, 193.05, 193.61, 193.89, 192.83], - [~N[2023-12-25 00:00:00], "AAPL", 37149570, 193.60, 195.18, 195.41, 192.97], - [~N[2023-12-24 00:00:00], "AAPL", 46482550, 194.68, 196.10, 197.08, 193.50], - [~N[2023-12-23 00:00:00], "AAPL", 52242820, 194.83, 196.90, 197.68, 194.83], - [~N[2023-12-22 00:00:00], "AAPL", 40714050, 196.94, 196.16, 196.95, 195.89], - [~N[2023-12-21 00:00:00], "AAPL", 55751860, 195.89, 196.09, 196.63, 194.39], + [~N[2023-12-28 00:00:00], "AAPL", 34049900, 194.14, 194.66, 193.17, 193.58], + [~N[2023-12-27 00:00:00], "AAPL", 48087680, 192.49, 193.50, 191.09, 193.15], + [~N[2023-12-26 00:00:00], "AAPL", 28919310, 193.61, 193.89, 192.83, 193.05], + [~N[2023-12-25 00:00:00], "AAPL", 37149570, 195.18, 195.41, 192.97, 193.60], + [~N[2023-12-24 00:00:00], "AAPL", 46482550, 196.10, 197.08, 193.50, 194.68], + [~N[2023-12-23 00:00:00], "AAPL", 52242820, 196.90, 197.68, 194.83, 194.83], + [~N[2023-12-22 00:00:00], "AAPL", 40714050, 196.16, 196.95, 195.89, 196.94], + [~N[2023-12-21 00:00:00], "AAPL", 55751860, 196.09, 196.63, 194.39, 195.89], ] - dataset = Dataset.new(data, ["Date", "Ticker", "Volume", "Close", "Open", "High", "Low"]) + dataset = Dataset.new(data, ["Date", "Ticker", "Volume", "Open", "High", "Low", "Close"]) opts = [ mapping: %{datetime: "Date", open: "Open", high: "High", low: "Low", close: "Close"}, @@ -122,38 +157,37 @@ defmodule Contex.OHLC do [] end - defp set_option(%__MODULE__{options: options} = plot, key, value) do - options = Keyword.put(options, key, value) - - %{plot | options: options} + @spec set_option(t(), atom(), any()) :: t() + defp set_option(plot, key, value) do + update(plot, :options, &Keyword.put(&1, key, value)) end - defp get_option(%__MODULE__{options: options}, key) do - Keyword.get(options, key) + @spec get_option(t(), atom()) :: term() + defp get_option(plot, key) do + Keyword.get(plot.options, key) end @doc false def to_svg(%__MODULE__{} = plot, plot_options) do plot = prepare_scales(plot) - x_scale = plot.x_scale - y_scale = plot.y_scale + [x_scale, y_scale] <~ plot plot_options = Map.merge(@default_plot_options, plot_options) x_axis_svg = if plot_options.show_x_axis, - do: - get_x_axis(x_scale, plot) - |> Axis.to_svg(), - else: "" + do: + get_x_axis(x_scale, plot) + |> Axis.to_svg(), + else: "" y_axis_svg = if plot_options.show_y_axis, - do: - Axis.new_left_axis(y_scale) - |> Axis.set_offset(get_option(plot, :width)) - |> Axis.to_svg(), - else: "" + do: + Axis.new_left_axis(y_scale) + |> Axis.set_offset(get_option(plot, :width)) + |> Axis.to_svg(), + else: "" [ x_axis_svg, @@ -164,41 +198,39 @@ defmodule Contex.OHLC do ] end - @green "00AA00" - @red "AA0000" - @grey "444444" - @bar_width 2 - - defp render_data(%__MODULE__{dataset: dataset} = plot) do - style = get_option(plot, :style) + @spec render_data(t()) :: [rendered_row()] + defp render_data(plot) do + [dataset] <~ plot dataset.data - |> Enum.map(fn row -> render_row(plot, row, style) end) + |> Enum.map(fn row -> render_row(plot, row) end) end - defp render_row(%__MODULE__{mapping: mapping, transforms: transforms}, row, style) do - accessors = mapping.accessors + @spec render_row(t(), row()) :: rendered_row() + defp render_row(plot, row) do + [transforms, mapping: [accessors], options: options = %{}] <~ plot x = accessors.datetime.(row) |> transforms.x.() - y_map = get_scaled_y_vals(row, accessors, transforms) - - colour = get_colour(y_map) - - draw_row(x, y_map, colour, style) + color = get_colour(get_y_vals(row, accessors), plot) + draw_row(options, x, get_y_vals(row, accessors, transforms.y), color) end - defp draw_row(x, y_map, colour, :candle) do - # We'll draw a grey line from low to high, then overlay a coloured rect - # for open / close - open = y_map.open - low = y_map.low - high = y_map.high - close = y_map.close + # Draws a grey line from low to high, then overlay a coloured rect + # for open / close if `:candle` style. + # Draws a grey line from low to high, and tick from left for open + # and to right for close if `:tick` style. + @spec draw_row(map(), number(), y_vals(), color()) :: rendered_row() + defp draw_row(options, x, y_map, colour) - bar_x = {x - @bar_width, x + @bar_width} + defp draw_row(%{style: :candle} = options, x, y_map, colour) do + [zoom] <~ options + [body_width] <~ @zoom_levels[zoom] + [open, high, low, close] <~ y_map + + bar_x = {x - body_width, x + body_width} bar_opts = [fill: colour] [ @@ -207,41 +239,49 @@ defmodule Contex.OHLC do ] end - defp draw_row(x, y_map, colour, :tick) do - # We'll draw a grey line from low to high, and tick from left for open - # and to right for close - open = y_map.open - low = y_map.low - high = y_map.high - close = y_map.close + defp draw_row(%{style: :tick} = options, x, y_map, colour) do + [zoom] <~ options + [body_width] <~ @zoom_levels[zoom] + [open, high, low, close] <~ y_map style = ~s|style="stroke: ##{colour}"| [ ~s||, - ~s||, - ~s|| + ~s||, + ~s|| ] end - defp get_scaled_y_vals(row, accessors, transforms) do + @spec get_y_vals(row(), accessors, transforms_y) :: y_vals() + when accessors: %{atom() => (row() -> number())}, + transforms_y: (number() -> number()) + defp get_y_vals(row, accessors, transforms_y \\ & &1) do [:open, :high, :low, :close] |> Enum.map(fn col -> - y = accessors[col].(row) |> transforms.y.() + y = + accessors[col].(row) + |> transforms_y.() {col, y} end) |> Enum.into(%{}) end - defp get_colour(%{open: open, close: close}) do + # todo: remove shadow color redundancy once body border is implemented + @spec get_colour(y_vals(), t()) :: color() + defp get_colour(y_map, plot) do + [bull_color, bear_color, shadow_color] <~ plot.options + [open, close] <~ y_map + cond do - close > open -> @green - close < open -> @red - true -> @grey + close > open -> bull_color + close < open -> bear_color + true -> shadow_color end end + @spec get_x_axis(Contex.TimeScale.t(), t()) :: Contex.Axis.t() defp get_x_axis(x_scale, plot) do rotation = case get_option(plot, :axis_label_rotation) do @@ -265,6 +305,7 @@ defmodule Contex.OHLC do |> prepare_y_scale() end + @spec prepare_x_scale(t()) :: t() defp prepare_x_scale(%__MODULE__{dataset: dataset, mapping: mapping} = plot) do x_col_name = mapping.column_map[:datetime] width = get_option(plot, :width) @@ -291,14 +332,11 @@ defmodule Contex.OHLC do |> Scale.set_range(r_min, r_max) end - defp prepare_y_scale(%__MODULE__{dataset: dataset, mapping: mapping} = plot) do - y_col_names = [ - mapping.column_map[:open], - mapping.column_map[:high], - mapping.column_map[:low], - mapping.column_map[:close] - ] + @spec prepare_y_scale(t()) :: t() + defp prepare_y_scale(plot) do + [dataset, mapping: [column_map]] <~ plot + y_col_names = Enum.map([:open, :high, :low, :close], &column_map[&1]) height = get_option(plot, :height) custom_y_scale = get_option(plot, :custom_y_scale) @@ -335,4 +373,9 @@ defmodule Contex.OHLC do combiner.(acc_extents, inner_extents) end) end + + @spec update(t(), atom(), (term() -> term())) :: t() + defp update(plot, field, updater) do + struct!(plot, %{field => updater.(Map.fetch!(plot, field))}) + end end diff --git a/mix.exs b/mix.exs index 1e35664..b0fd3b8 100644 --- a/mix.exs +++ b/mix.exs @@ -34,6 +34,7 @@ defmodule Contex.MixProject do {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:sweet_xml, "~> 0.7.3", only: :test}, {:floki, "~> 0.34.2", only: :test}, + {:extructure, "~> 1.0"}, {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 3eabc7c..2b51e10 100644 --- a/mix.lock +++ b/mix.lock @@ -4,6 +4,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, + "extructure": {:hex, :extructure, "1.0.0", "7fb05a7d05094bb381ae753226f8ceca6adbbaa5bd0c90ebe3d286f20d87fc1e", [:mix], [], "hexpm", "5f67c55786867a92c549aaaace29c898c2cc02cc01b69f2192de7b7bdb5c8078"}, "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, From 8c5ee81b83e03a6e25ab9ab1e9dae7a19d9ffa9c Mon Sep 17 00:00:00 2001 From: damir Date: Sat, 6 Jan 2024 23:43:58 +0100 Subject: [PATCH 2/4] make shadow_color work; add crisp_edges option; make colorized_bars optional; make body border optional. --- lib/chart/ohlc.ex | 59 +++++++++++++++++++++++++++++++---------------- lib/chart/svg.ex | 3 +++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/lib/chart/ohlc.ex b/lib/chart/ohlc.ex index 33a5fa2..deabf5a 100644 --- a/lib/chart/ohlc.ex +++ b/lib/chart/ohlc.ex @@ -64,7 +64,7 @@ defmodule Contex.OHLC do @green "00AA00" @red "AA0000" - @grey "444444" + @black "000000" @required_mappings [ datetime: :exactly_one, @@ -86,7 +86,7 @@ defmodule Contex.OHLC do zoom: 3, bull_color: @green, bear_color: @red, - shadow_color: @grey + shadow_color: @black ] @default_plot_options %{ @@ -176,18 +176,18 @@ defmodule Contex.OHLC do x_axis_svg = if plot_options.show_x_axis, - do: - get_x_axis(x_scale, plot) - |> Axis.to_svg(), - else: "" + do: + get_x_axis(x_scale, plot) + |> Axis.to_svg(), + else: "" y_axis_svg = if plot_options.show_y_axis, - do: - Axis.new_left_axis(y_scale) - |> Axis.set_offset(get_option(plot, :width)) - |> Axis.to_svg(), - else: "" + do: + Axis.new_left_axis(y_scale) + |> Axis.set_offset(get_option(plot, :width)) + |> Axis.to_svg(), + else: "" [ x_axis_svg, @@ -223,28 +223,47 @@ defmodule Contex.OHLC do # Draws a grey line from low to high, and tick from left for open # and to right for close if `:tick` style. @spec draw_row(map(), number(), y_vals(), color()) :: rendered_row() - defp draw_row(options, x, y_map, colour) + defp draw_row(options, x, y_map, body_color) - defp draw_row(%{style: :candle} = options, x, y_map, colour) do - [zoom] <~ options + defp draw_row(%{style: :candle} = options, x, y_map, body_color) do + [zoom, shadow_color, crisp_edges(false), body_border(false)] <~ options [body_width] <~ @zoom_levels[zoom] [open, high, low, close] <~ y_map bar_x = {x - body_width, x + body_width} - bar_opts = [fill: colour] + + body_opts = + [ + fill: body_color, + stroke: body_border && shadow_color, + shape_rendering: crisp_edges && "crispEdges" + ] + |> Enum.filter(&elem(&1, 1)) + + style = + [ + ~s|style="stroke: ##{shadow_color}"|, + (crisp_edges && ~s| shape-rendering="crispEdges"|) || "" + ] + |> Enum.join() [ - ~s||, - rect(bar_x, {open, close}, "", bar_opts) + ~s||, + rect(bar_x, {open, close}, "", body_opts) ] end - defp draw_row(%{style: :tick} = options, x, y_map, colour) do - [zoom] <~ options + defp draw_row(%{style: :tick} = options, x, y_map, body_color) do + [zoom, shadow_color, crisp_edges(false), colorized_bars(false)] <~ options [body_width] <~ @zoom_levels[zoom] [open, high, low, close] <~ y_map - style = ~s|style="stroke: ##{colour}"| + style = + [ + ~s|style="stroke: ##{colorized_bars && body_color || shadow_color}"|, + (crisp_edges && ~s| shape-rendering="crispEdges"|) || "" + ] + |> Enum.join() [ ~s||, diff --git a/lib/chart/svg.ex b/lib/chart/svg.ex index 55f16d0..6547df3 100644 --- a/lib/chart/svg.ex +++ b/lib/chart/svg.ex @@ -221,6 +221,9 @@ defmodule Contex.SVG do defp opts_to_attrs([{:marker_end, val} | t], attrs), do: opts_to_attrs(t, [[" marker-end=\"", val, "\""] | attrs]) + defp opts_to_attrs([{:shape_rendering, val} | t], attrs), + do: opts_to_attrs(t, [[" shape-rendering=\"", val, "\""] | attrs]) + defp opts_to_attrs([{key, val} | t], attrs) when is_atom(key), do: opts_to_attrs(t, [[" ", Atom.to_string(key), "=\"", clean(val), "\""] | attrs]) From 1bf65d89e4e970c500c897b31a8e8bfb1a049e77 Mon Sep 17 00:00:00 2001 From: damir Date: Sun, 7 Jan 2024 11:05:30 +0100 Subject: [PATCH 3/4] remove redundant invocation of get_y_vals --- lib/chart/ohlc.ex | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/chart/ohlc.ex b/lib/chart/ohlc.ex index deabf5a..066a60a 100644 --- a/lib/chart/ohlc.ex +++ b/lib/chart/ohlc.ex @@ -28,7 +28,6 @@ defmodule Contex.OHLC do """ # todo: - # - invert row order and update docs to reflect it (i.e. from past to present not the other way around) # - refactor :tick into :bar import Contex.SVG @@ -214,8 +213,14 @@ defmodule Contex.OHLC do accessors.datetime.(row) |> transforms.x.() - color = get_colour(get_y_vals(row, accessors), plot) - draw_row(options, x, get_y_vals(row, accessors, transforms.y), color) + y_vals = get_y_vals(row, accessors) + + scaled_y_vals = Map.new( y_vals, fn { k, v} -> + { k, transforms.y.( v)} + end) + + color = get_colour(y_vals, plot) + draw_row(options, x, scaled_y_vals, color) end # Draws a grey line from low to high, then overlay a coloured rect @@ -272,22 +277,17 @@ defmodule Contex.OHLC do ] end - @spec get_y_vals(row(), accessors, transforms_y) :: y_vals() - when accessors: %{atom() => (row() -> number())}, - transforms_y: (number() -> number()) - defp get_y_vals(row, accessors, transforms_y \\ & &1) do + @spec get_y_vals(row(), %{atom() => (row() -> number())}) :: y_vals() + defp get_y_vals(row, accessors) do [:open, :high, :low, :close] |> Enum.map(fn col -> - y = - accessors[col].(row) - |> transforms_y.() + y = accessors[col].(row) {col, y} end) |> Enum.into(%{}) end - # todo: remove shadow color redundancy once body border is implemented @spec get_colour(y_vals(), t()) :: color() defp get_colour(y_map, plot) do [bull_color, bear_color, shadow_color] <~ plot.options From e32997d589d800e25d1447ed0a8c68779924d215 Mon Sep 17 00:00:00 2001 From: damir Date: Tue, 16 Jan 2024 12:25:41 +0100 Subject: [PATCH 4/4] format --- lib/chart/ohlc.ex | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/chart/ohlc.ex b/lib/chart/ohlc.ex index 066a60a..17aeaa3 100644 --- a/lib/chart/ohlc.ex +++ b/lib/chart/ohlc.ex @@ -215,9 +215,10 @@ defmodule Contex.OHLC do y_vals = get_y_vals(row, accessors) - scaled_y_vals = Map.new( y_vals, fn { k, v} -> - { k, transforms.y.( v)} - end) + scaled_y_vals = + Map.new(y_vals, fn {k, v} -> + {k, transforms.y.(v)} + end) color = get_colour(y_vals, plot) draw_row(options, x, scaled_y_vals, color) @@ -265,7 +266,7 @@ defmodule Contex.OHLC do style = [ - ~s|style="stroke: ##{colorized_bars && body_color || shadow_color}"|, + ~s|style="stroke: ##{(colorized_bars && body_color) || shadow_color}"|, (crisp_edges && ~s| shape-rendering="crispEdges"|) || "" ] |> Enum.join()