diff --git a/README.md b/README.md
index 13e1059..2e374b3 100755
--- a/README.md
+++ b/README.md
@@ -85,9 +85,11 @@ $chart->options([
# Advanced Chartjs options
-The basic options() method allows you to add simple key-value pair based options. Using the optionsRaw() method it's possible to add more complex nested Chartjs options in raw json format and to used nested calls to the options for plugins:
+The basic options() method allows you to add simple key-value pair based options. Using the optionsRaw() method it's possible to add more complex nested Chartjs options in raw JavaScript format and to use JavaScript functions for callbacks:
-Passing string format raw options to the chart like a json:
+## Basic Raw Options
+
+Passing string format raw options to the chart like a JavaScript object:
```php
$chart->optionsRaw("{
scales: {
@@ -113,6 +115,102 @@ $chart->optionsRaw("{
}");
```
+## Number Formatting with JavaScript Functions
+
+For advanced number formatting (like currency display), you can use JavaScript functions in your optionsRaw:
+
+```php
+$chart->optionsRaw('{
+ responsive: true,
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "R$ " + context.parsed.y.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ callback: function(value, index, values) {
+ return "R$ " + value.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+}');
+```
+
+For US dollar formatting:
+```php
+$chart->optionsRaw('{
+ responsive: true,
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "$" + context.parsed.y.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ ticks: {
+ callback: function(value, index, values) {
+ return "$" + value.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+}');
+```
+
+## Troubleshooting optionsRaw
+
+If your number formatting isn't working:
+
+1. **Check JavaScript syntax**: Ensure your JavaScript object syntax is valid. You can use either quoted (`"responsive": true`) or unquoted (`responsive: true`) property names.
+
+2. **Verify function placement**: Callback functions should be placed in the correct Chart.js option paths:
+ - Tooltip formatting: `plugins.tooltip.callbacks.label`
+ - Axis tick formatting: `scales.y.ticks.callback` (or `scales.x.ticks.callback`)
+
+3. **Test with browser console**: Open your browser's developer console to check for JavaScript errors that might prevent the functions from executing.
+
+4. **Check Chart.js version compatibility**: Ensure your callback function syntax matches your Chart.js version. This package supports Chart.js v2, v3, and v4.
+
+5. **Validate locale support**: Make sure the locale you're using (like "pt-BR") is supported by the browser's `toLocaleString()` method.
+
# Examples
1 - Line Chart:
diff --git a/examples/ChartController.php b/examples/ChartController.php
new file mode 100644
index 0000000..226432a
--- /dev/null
+++ b/examples/ChartController.php
@@ -0,0 +1,217 @@
+name('salesChart')
+ ->type('bar')
+ ->size(['width' => 600, 'height' => 400])
+ ->labels(['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho'])
+ ->datasets([
+ [
+ 'label' => 'Vendas 2024',
+ 'data' => [12345.67, 23456.78, 18945.32, 29876.43, 35467.89, 41234.56],
+ 'backgroundColor' => 'rgba(54, 162, 235, 0.6)',
+ 'borderColor' => 'rgba(54, 162, 235, 1)',
+ 'borderWidth' => 1,
+ ],
+ [
+ 'label' => 'Vendas 2023',
+ 'data' => [10234.56, 19876.54, 16543.21, 25432.10, 31098.76, 37654.32],
+ 'backgroundColor' => 'rgba(255, 99, 132, 0.6)',
+ 'borderColor' => 'rgba(255, 99, 132, 1)',
+ 'borderWidth' => 1,
+ ]
+ ])
+ ->optionsRaw('{
+ responsive: true,
+ plugins: {
+ title: {
+ display: true,
+ text: "Vendas Mensais"
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "R$ " + context.parsed.y.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ title: {
+ display: true,
+ text: "Valores (R$)"
+ },
+ ticks: {
+ callback: function(value, index, values) {
+ return "R$ " + value.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ },
+ x: {
+ title: {
+ display: true,
+ text: "Meses"
+ }
+ }
+ }
+ }');
+
+ return view('charts.sales', compact('chart'));
+ }
+
+ /**
+ * US Dollar formatting example
+ */
+ public function usDollarFormatting()
+ {
+ $chart = Chartjs::build()
+ ->name('revenueChart')
+ ->type('line')
+ ->size(['width' => 600, 'height' => 400])
+ ->labels(['Q1', 'Q2', 'Q3', 'Q4'])
+ ->datasets([
+ [
+ 'label' => 'Revenue 2024',
+ 'data' => [125000.50, 187500.75, 156750.25, 234000.00],
+ 'backgroundColor' => 'rgba(75, 192, 192, 0.2)',
+ 'borderColor' => 'rgba(75, 192, 192, 1)',
+ 'borderWidth' => 2,
+ 'fill' => false,
+ ]
+ ])
+ ->optionsRaw('{
+ responsive: true,
+ plugins: {
+ title: {
+ display: true,
+ text: "Quarterly Revenue"
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "$" + context.parsed.y.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ title: {
+ display: true,
+ text: "Revenue (USD)"
+ },
+ ticks: {
+ callback: function(value, index, values) {
+ return "$" + value.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+ }');
+
+ return view('charts.revenue', compact('chart'));
+ }
+
+ /**
+ * Alternative approach using quoted JSON syntax (user's original approach)
+ * This also works perfectly fine
+ */
+ public function quotedSyntaxExample()
+ {
+ $chart = Chartjs::build()
+ ->name('quotedChart')
+ ->type('bar')
+ ->size(['width' => 600, 'height' => 400])
+ ->labels(['Jan', 'Feb', 'Mar'])
+ ->datasets([
+ [
+ 'label' => 'Sales',
+ 'data' => [1234.56, 2345.67, 3456.78],
+ 'backgroundColor' => 'rgba(153, 102, 255, 0.6)',
+ ]
+ ])
+ ->optionsRaw('{
+ "responsive": true,
+ "plugins": {
+ "tooltip": {
+ "callbacks": {
+ "label": function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "R$ " + context.parsed.y.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ "scales": {
+ "y": {
+ "ticks": {
+ "callback": function(value, index, values) {
+ return "R$ " + value.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+ }');
+
+ return view('charts.quoted-example', compact('chart'));
+ }
+}
\ No newline at end of file
diff --git a/examples/sales.blade.php b/examples/sales.blade.php
new file mode 100644
index 0000000..608b002
--- /dev/null
+++ b/examples/sales.blade.php
@@ -0,0 +1,42 @@
+{{-- resources/views/charts/sales.blade.php --}}
+@extends('layouts.app')
+
+@section('content')
+
+
+
+
+
+
+
+
Monthly Sales with Brazilian Real Formatting
+
This chart demonstrates proper number formatting in tooltips and Y-axis labels.
+
+ {{-- Laravel ChartJS component --}}
+
+
+
+
+
+
Features demonstrated:
+
+ - ✓ Brazilian Real currency formatting (R$ 1.234,56)
+ - ✓ Custom tooltip callbacks with locale-specific number formatting
+ - ✓ Y-axis tick formatting with currency symbols
+ - ✓ Responsive chart design
+ - ✓ Multi-dataset support
+
+
+
+
+
Technical Details:
+
This chart uses the optionsRaw() method with JavaScript functions
+ to format numbers using toLocaleString("pt-BR") for Brazilian
+ Portuguese formatting with proper decimal places.
+
+
+
+
+
+
+@endsection
\ No newline at end of file
diff --git a/src/Builder.php b/src/Builder.php
index 85c4d56..49b1797 100644
--- a/src/Builder.php
+++ b/src/Builder.php
@@ -151,15 +151,45 @@ public function options(array $options)
public function optionsRaw(string|array $optionsRaw)
{
if (is_array($optionsRaw)) {
- $this->set('optionsRaw', json_encode($optionsRaw, true));
+ $this->set('optionsRaw', json_encode($optionsRaw, JSON_THROW_ON_ERROR));
return $this;
}
+ // Validate that the string contains valid JavaScript object syntax
+ $this->validateJavaScriptOptions($optionsRaw);
+
$this->set('optionsRaw', $optionsRaw);
return $this;
}
+ /**
+ * Validates JavaScript options for common issues
+ *
+ * @param string $options
+ * @throws \InvalidArgumentException
+ */
+ private function validateJavaScriptOptions(string $options)
+ {
+ $trimmed = trim($options);
+
+ // Check if it starts and ends with braces
+ if (!str_starts_with($trimmed, '{') || !str_ends_with($trimmed, '}')) {
+ throw new \InvalidArgumentException('optionsRaw must be a valid JavaScript object (should start with { and end with })');
+ }
+
+ // Check for common mistakes
+ if (str_contains($options, 'function(') && str_contains($options, '"function(')) {
+ throw new \InvalidArgumentException('JavaScript functions should not be quoted in optionsRaw');
+ }
+
+ // Try to detect if it's meant to be JSON but contains functions
+ if (preg_match('/^\s*{\s*"/', $trimmed) && str_contains($options, 'function(')) {
+ // This looks like JSON syntax but contains functions - warn the user
+ // This is actually valid JavaScript, so we'll allow it but could add a warning in the future
+ }
+ }
+
/**
* @return mixed
*/
@@ -171,7 +201,7 @@ public function render()
$optionsFallback = "{}";
- $optionsSimple = $chart['options'] ? json_encode($chart['options'], true) : null;
+ $optionsSimple = $chart['options'] ? json_encode($chart['options'], JSON_THROW_ON_ERROR) : null;
$options = $chart['optionsRaw'] ?? $optionsSimple ?? $optionsFallback;
diff --git a/src/Examples/NumberFormattingExample.php b/src/Examples/NumberFormattingExample.php
new file mode 100644
index 0000000..d778187
--- /dev/null
+++ b/src/Examples/NumberFormattingExample.php
@@ -0,0 +1,163 @@
+name('formattedChart')
+ ->type('bar')
+ ->size(['width' => 400, 'height' => 200])
+ ->labels(['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio'])
+ ->datasets([
+ [
+ 'label' => 'Vendas (R$)',
+ 'data' => [1234.56, 2345.67, 3456.78, 4567.89, 5678.90],
+ 'backgroundColor' => 'rgba(54, 162, 235, 0.2)',
+ 'borderColor' => 'rgba(54, 162, 235, 1)',
+ 'borderWidth' => 1,
+ ]
+ ])
+ ->optionsRaw('{
+ responsive: true,
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "R$ " + context.parsed.y.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ callback: function(value, index, values) {
+ return "R$ " + value.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+ }');
+
+ return $chart;
+ }
+
+ /**
+ * Alternative approach using array-based options (will be JSON encoded)
+ * Note: This approach cannot include JavaScript functions
+ */
+ public static function createSimpleFormattedChart()
+ {
+ $chart = Chartjs::build()
+ ->name('simpleChart')
+ ->type('line')
+ ->size(['width' => 400, 'height' => 200])
+ ->labels(['Jan', 'Feb', 'Mar', 'Apr', 'May'])
+ ->datasets([
+ [
+ 'label' => 'Revenue',
+ 'data' => [1000, 2000, 1500, 3000, 2500],
+ 'backgroundColor' => 'rgba(255, 99, 132, 0.2)',
+ 'borderColor' => 'rgba(255, 99, 132, 1)',
+ 'borderWidth' => 2,
+ 'fill' => false,
+ ]
+ ])
+ ->options([
+ 'responsive' => true,
+ 'scales' => [
+ 'y' => [
+ 'beginAtZero' => true,
+ ]
+ ]
+ ]);
+
+ return $chart;
+ }
+
+ /**
+ * Example for US dollar formatting
+ */
+ public static function createUSDollarChart()
+ {
+ $chart = Chartjs::build()
+ ->name('usdChart')
+ ->type('bar')
+ ->size(['width' => 400, 'height' => 200])
+ ->labels(['Q1', 'Q2', 'Q3', 'Q4'])
+ ->datasets([
+ [
+ 'label' => 'Revenue (USD)',
+ 'data' => [12345.67, 23456.78, 34567.89, 45678.90],
+ 'backgroundColor' => 'rgba(75, 192, 192, 0.2)',
+ 'borderColor' => 'rgba(75, 192, 192, 1)',
+ 'borderWidth' => 1,
+ ]
+ ])
+ ->optionsRaw('{
+ responsive: true,
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "$" + context.parsed.y.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ callback: function(value, index, values) {
+ return "$" + value.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+ }');
+
+ return $chart;
+ }
+}
\ No newline at end of file
diff --git a/tests/Feature/OptionsRawTest.php b/tests/Feature/OptionsRawTest.php
new file mode 100644
index 0000000..d5fb71d
--- /dev/null
+++ b/tests/Feature/OptionsRawTest.php
@@ -0,0 +1,197 @@
+name('chart1')->optionsRaw($rawOptionsWithFunctions);
+
+ expect($result)->toBeInstanceOf(Builder::class);
+
+ // Verify the options are stored correctly
+ $storedOptions = $builder->get('optionsRaw');
+ expect($storedOptions)->toBe($rawOptionsWithFunctions);
+ expect($storedOptions)->toContain('function(context)');
+ expect($storedOptions)->toContain('toLocaleString("pt-BR"');
+ expect($storedOptions)->toContain('R$ ');
+});
+
+test('it can handle simple JSON options without functions', function () {
+ $builder = new Builder();
+
+ $simpleOptions = '{
+ "responsive": true,
+ "plugins": {
+ "legend": {
+ "display": false
+ }
+ }
+ }';
+
+ $result = $builder->name('chart2')->optionsRaw($simpleOptions);
+
+ expect($result)->toBeInstanceOf(Builder::class);
+
+ // Verify the options are stored correctly
+ $storedOptions = $builder->get('optionsRaw');
+ expect($storedOptions)->toBe($simpleOptions);
+ expect($storedOptions)->toContain('"responsive": true');
+ expect($storedOptions)->toContain('"display": false');
+});
+
+test('it handles array-based optionsRaw correctly', function () {
+ $builder = new Builder();
+
+ $arrayOptions = [
+ 'responsive' => true,
+ 'plugins' => [
+ 'legend' => [
+ 'display' => false
+ ]
+ ]
+ ];
+
+ $result = $builder->name('chart3')->optionsRaw($arrayOptions);
+
+ expect($result)->toBeInstanceOf(Builder::class);
+
+ // Should be JSON encoded
+ $storedOptions = $builder->get('optionsRaw');
+ expect($storedOptions)->toBeString();
+ expect($storedOptions)->toContain('"responsive":true');
+ expect($storedOptions)->toContain('"display":false');
+});
+
+test('it prioritizes optionsRaw over regular options', function () {
+ $builder = new Builder();
+
+ $builder->name('chart4')
+ ->options(['responsive' => false])
+ ->optionsRaw('{"responsive": true}');
+
+ // optionsRaw should take precedence
+ $storedRaw = $builder->get('optionsRaw');
+ $storedRegular = $builder->get('options');
+
+ expect($storedRaw)->toBe('{"responsive": true}');
+ expect($storedRegular)->toHaveKey('responsive', false);
+
+ // When rendering, optionsRaw should be used
+ // Note: This would need a proper Laravel environment to test fully
+});
+
+test('it handles javascript functions in optionsRaw string', function () {
+ $builder = new Builder();
+
+ // Test with the user's exact formatting issue
+ $jsWithFunctions = '{
+ responsive: true,
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ return "R$ " + context.parsed.y.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+ }';
+
+ $result = $builder->name('chart5')->optionsRaw($jsWithFunctions);
+
+ expect($result)->toBeInstanceOf(Builder::class);
+
+ $storedOptions = $builder->get('optionsRaw');
+ expect($storedOptions)->toBe($jsWithFunctions);
+ expect($storedOptions)->toContain('function(context)');
+ expect($storedOptions)->toContain('toLocaleString("pt-BR"');
+ expect($storedOptions)->toContain('minimumFractionDigits: 2');
+});
+
+test('it validates optionsRaw input format', function () {
+ $builder = new Builder();
+
+ // Should reject invalid input that doesn't start/end with braces
+ expect(function() use ($builder) {
+ $builder->name('invalid1')->optionsRaw('responsive: true');
+ })->toThrow(InvalidArgumentException::class);
+
+ // Should reject quoted functions
+ expect(function() use ($builder) {
+ $builder->name('invalid2')->optionsRaw('{"callback": "function() { return true; }"}');
+ })->toThrow(InvalidArgumentException::class);
+});
+
+test('it accepts both quoted and unquoted property syntax', function () {
+ $builder1 = new Builder();
+ $builder2 = new Builder();
+
+ // Quoted JSON-style properties (user's approach)
+ $quotedSyntax = '{
+ "responsive": true,
+ "plugins": {
+ "legend": {
+ "display": false
+ }
+ }
+ }';
+
+ // Unquoted JavaScript object properties (README approach)
+ $unquotedSyntax = '{
+ responsive: true,
+ plugins: {
+ legend: {
+ display: false
+ }
+ }
+ }';
+
+ // Both should work
+ expect(function() use ($builder1, $quotedSyntax) {
+ $builder1->name('quoted')->optionsRaw($quotedSyntax);
+ })->not->toThrow();
+
+ expect(function() use ($builder2, $unquotedSyntax) {
+ $builder2->name('unquoted')->optionsRaw($unquotedSyntax);
+ })->not->toThrow();
+});
\ No newline at end of file
diff --git a/tests/Feature/UserIssueReproductionTest.php b/tests/Feature/UserIssueReproductionTest.php
new file mode 100644
index 0000000..e6e40e1
--- /dev/null
+++ b/tests/Feature/UserIssueReproductionTest.php
@@ -0,0 +1,126 @@
+name('userChart')
+ ->type('bar')
+ ->size(['width' => 400, 'height' => 200])
+ ->labels(['Janeiro', 'Fevereiro', 'Março'])
+ ->datasets([
+ [
+ 'label' => 'Vendas',
+ 'data' => [1234.56, 2345.67, 3456.78],
+ 'backgroundColor' => 'rgba(54, 162, 235, 0.2)',
+ 'borderColor' => 'rgba(54, 162, 235, 1)',
+ 'borderWidth' => 1
+ ]
+ ])
+ ->optionsRaw($userOptionsRaw);
+
+ expect($chart)->toBeInstanceOf(Builder::class);
+
+ // Verify the options are stored correctly
+ $storedOptions = $chart->get('optionsRaw');
+ expect($storedOptions)->toBe($userOptionsRaw);
+
+ // Verify it contains the formatting functions
+ expect($storedOptions)->toContain('function(context)');
+ expect($storedOptions)->toContain('function(value, index, values)');
+ expect($storedOptions)->toContain('toLocaleString("pt-BR"');
+ expect($storedOptions)->toContain('minimumFractionDigits: 2');
+ expect($storedOptions)->toContain('maximumFractionDigits: 2');
+ expect($storedOptions)->toContain('R$ ');
+});
+
+test('working number formatting examples', function () {
+ $builder = new Builder();
+
+ // Example using unquoted syntax (recommended)
+ $recommendedSyntax = '{
+ responsive: true,
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || "";
+ if (label) {
+ label += ": ";
+ }
+ if (context.parsed.y !== null) {
+ label += "R$ " + context.parsed.y.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ return label;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ callback: function(value, index, values) {
+ return "R$ " + value.toLocaleString("pt-BR", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+ }
+ }
+ }
+ }
+ }';
+
+ $chart = $builder
+ ->name('recommendedChart')
+ ->type('line')
+ ->optionsRaw($recommendedSyntax);
+
+ expect($chart)->toBeInstanceOf(Builder::class);
+
+ $storedOptions = $chart->get('optionsRaw');
+ expect($storedOptions)->toBe($recommendedSyntax);
+ expect($storedOptions)->toContain('beginAtZero: true');
+});
\ No newline at end of file