diff --git a/docs/app/views/docs/chart.rb b/docs/app/views/docs/chart.rb index 62d032e63..233ecb6fc 100644 --- a/docs/app/views/docs/chart.rb +++ b/docs/app/views/docs/chart.rb @@ -17,6 +17,14 @@ def view_template Heading(level: 2) { "Usage" } + Text do + plain "Datasets are colored with the " + InlineCode { "--chart-1" } + plain " to " + InlineCode { "--chart-5" } + plain " theme variables, in order. Pie, doughnut and polar area charts color each slice instead. Colors set on a dataset take precedence." + end + render Docs::VisualCodeExample.new(title: "Bar Chart", context: self) do <<~RUBY options = { @@ -42,6 +50,23 @@ def view_template RUBY end + render Docs::VisualCodeExample.new(title: "Multiple datasets", context: self) do + <<~RUBY + options = { + type: 'bar', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June'], + datasets: [ + { label: 'Desktop', data: [186, 305, 237, 73, 209, 214] }, + { label: 'Mobile', data: [80, 200, 120, 190, 130, 140] } + ] + }, + } + + Chart(options: options) + RUBY + end + render Docs::VisualCodeExample.new(title: "Line Graph", context: self) do <<~RUBY options = { diff --git a/gem/lib/ruby_ui/chart/chart_controller.js b/gem/lib/ruby_ui/chart/chart_controller.js index 18034447f..8e99dc3d8 100644 --- a/gem/lib/ruby_ui/chart/chart_controller.js +++ b/gem/lib/ruby_ui/chart/chart_controller.js @@ -32,17 +32,33 @@ export default class extends Controller { this.setDefaultColorsForChart() } + // Theme tokens hold complete CSS colors (e.g. oklch(...)), which the canvas accepts as-is getThemeColor(name) { - const color = getComputedStyle(document.documentElement).getPropertyValue(`--${name}`) - const [hue, saturation, lightness] = color.split(' ') - return `hsl(${hue}, ${saturation}, ${lightness})` + return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim() } - defaultThemeColor() { + // Cycles through the --chart-1..5 theme tokens, like shadcn's chart examples + getChartColor(index) { + return this.getThemeColor(`chart-${(index % 5) + 1}`) + } + + // Pie-like charts color each slice; the others color each dataset + defaultThemeColor(dataset, index) { + const type = dataset.type || this.optionsValue.type + + if (['pie', 'doughnut', 'polarArea'].includes(type)) { + return { + backgroundColor: Object.values(dataset.data || {}).map((_, i) => this.getChartColor(i)), + borderColor: this.getThemeColor('background'), + borderWidth: 1, + } + } + + const color = this.getChartColor(index) return { - backgroundColor: this.getThemeColor('background'), - hoverBackgroundColor: this.getThemeColor('accent'), - borderColor: this.getThemeColor('primary'), + backgroundColor: color, + hoverBackgroundColor: color, + borderColor: color, borderWidth: 1, } } @@ -66,7 +82,7 @@ export default class extends Controller { Chart.defaults.plugins.legend.labels.boxHeight = 12 Chart.defaults.plugins.legend.labels.borderWidth = 0 Chart.defaults.plugins.legend.labels.useBorderRadius = true - Chart.defaults.plugins.legend.labels.borderRadius = this.getThemeColor('radius') + Chart.defaults.plugins.legend.labels.borderRadius = 2 // px, matches shadcn's rounded-[2px] legend indicator } // Function to refresh the chart @@ -91,9 +107,9 @@ export default class extends Controller { ...this.optionsValue, data: { ...this.optionsValue.data, - datasets: this.optionsValue.data.datasets.map((dataset) => { + datasets: this.optionsValue.data.datasets.map((dataset, index) => { return { - ...this.defaultThemeColor(), + ...this.defaultThemeColor(dataset, index), ...dataset, } }) diff --git a/gem/lib/ruby_ui/chart/chart_docs.rb b/gem/lib/ruby_ui/chart/chart_docs.rb index 6aaf88ed0..677f91c33 100644 --- a/gem/lib/ruby_ui/chart/chart_docs.rb +++ b/gem/lib/ruby_ui/chart/chart_docs.rb @@ -17,6 +17,14 @@ def view_template Heading(level: 2) { "Usage" } + Text do + plain "Datasets are colored with the " + InlineCode { "--chart-1" } + plain " to " + InlineCode { "--chart-5" } + plain " theme variables, in order. Pie, doughnut and polar area charts color each slice instead. Colors set on a dataset take precedence." + end + render Docs::VisualCodeExample.new(title: "Bar Chart", context: self) do <<~RUBY options = { @@ -42,6 +50,23 @@ def view_template RUBY end + render Docs::VisualCodeExample.new(title: "Multiple datasets", context: self) do + <<~RUBY + options = { + type: 'bar', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June'], + datasets: [ + { label: 'Desktop', data: [186, 305, 237, 73, 209, 214] }, + { label: 'Mobile', data: [80, 200, 120, 190, 130, 140] } + ] + }, + } + + Chart(options: options) + RUBY + end + render Docs::VisualCodeExample.new(title: "Line Graph", context: self) do <<~RUBY options = { diff --git a/mcp/data/registry.json b/mcp/data/registry.json index c5505d648..e9d638b2c 100644 --- a/mcp/data/registry.json +++ b/mcp/data/registry.json @@ -728,7 +728,7 @@ }, { "path": "chart_controller.js", - "content": "import { Controller } from \"@hotwired/stimulus\"\nimport Chart from 'chart.js/auto'\n\n// Chart controller\nexport default class extends Controller {\n static values = {\n options: {\n type: Object,\n default: {},\n }\n }\n\n // Function to initialize the chart when the controller is connected\n connect() {\n this.initDarkModeObserver()\n this.initChart()\n }\n\n disconnect() {\n this.darkModeObserver?.disconnect()\n this.chart?.destroy()\n }\n\n // Function to initialize the chart\n initChart() {\n this.setColors()\n const ctx = this.element.getContext('2d');\n this.chart = new Chart(ctx, this.mergeOptionsWithDefaults());\n }\n\n setColors() {\n this.setDefaultColorsForChart()\n }\n\n getThemeColor(name) {\n const color = getComputedStyle(document.documentElement).getPropertyValue(`--${name}`)\n const [hue, saturation, lightness] = color.split(' ')\n return `hsl(${hue}, ${saturation}, ${lightness})`\n }\n\n defaultThemeColor() {\n return {\n backgroundColor: this.getThemeColor('background'),\n hoverBackgroundColor: this.getThemeColor('accent'),\n borderColor: this.getThemeColor('primary'),\n borderWidth: 1,\n }\n }\n\n // Function to set chart default colors\n setDefaultColorsForChart() {\n Chart.defaults.color = this.getThemeColor('muted-foreground') // font color\n Chart.defaults.borderColor = this.getThemeColor('border') // border color\n Chart.defaults.backgroundColor = this.getThemeColor('background') // background color\n\n // tooltip colors\n Chart.defaults.plugins.tooltip.backgroundColor = this.getThemeColor('background')\n Chart.defaults.plugins.tooltip.borderColor = this.getThemeColor('border')\n Chart.defaults.plugins.tooltip.titleColor = this.getThemeColor('foreground')\n Chart.defaults.plugins.tooltip.bodyColor = this.getThemeColor('muted-foreground')\n Chart.defaults.plugins.tooltip.borderWidth = 1\n\n // legend\n // options.plugins.legend.labels\n Chart.defaults.plugins.legend.labels.boxWidth = 12\n Chart.defaults.plugins.legend.labels.boxHeight = 12\n Chart.defaults.plugins.legend.labels.borderWidth = 0\n Chart.defaults.plugins.legend.labels.useBorderRadius = true\n Chart.defaults.plugins.legend.labels.borderRadius = this.getThemeColor('radius')\n }\n\n // Function to refresh the chart\n refreshChart() {\n // Destroy the chart if it's a valid Chart.js instance\n this.chart?.destroy()\n // Reinitialize the chart\n this.initChart()\n }\n\n // Function to initialize the dark mode observer\n initDarkModeObserver() {\n this.darkModeObserver = new MutationObserver(() => {\n this.refreshChart()\n })\n this.darkModeObserver.observe(document.documentElement, { attributeFilter: ['class'] })\n }\n\n // Function to merge the options with the defaults\n mergeOptionsWithDefaults() {\n return {\n ...this.optionsValue,\n data: {\n ...this.optionsValue.data,\n datasets: this.optionsValue.data.datasets.map((dataset) => {\n return {\n ...this.defaultThemeColor(),\n ...dataset,\n }\n })\n }\n }\n }\n}\n" + "content": "import { Controller } from \"@hotwired/stimulus\"\nimport Chart from 'chart.js/auto'\n\n// Chart controller\nexport default class extends Controller {\n static values = {\n options: {\n type: Object,\n default: {},\n }\n }\n\n // Function to initialize the chart when the controller is connected\n connect() {\n this.initDarkModeObserver()\n this.initChart()\n }\n\n disconnect() {\n this.darkModeObserver?.disconnect()\n this.chart?.destroy()\n }\n\n // Function to initialize the chart\n initChart() {\n this.setColors()\n const ctx = this.element.getContext('2d');\n this.chart = new Chart(ctx, this.mergeOptionsWithDefaults());\n }\n\n setColors() {\n this.setDefaultColorsForChart()\n }\n\n // Theme tokens hold complete CSS colors (e.g. oklch(...)), which the canvas accepts as-is\n getThemeColor(name) {\n return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim()\n }\n\n // Cycles through the --chart-1..5 theme tokens, like shadcn's chart examples\n getChartColor(index) {\n return this.getThemeColor(`chart-${(index % 5) + 1}`)\n }\n\n // Pie-like charts color each slice; the others color each dataset\n defaultThemeColor(dataset, index) {\n const type = dataset.type || this.optionsValue.type\n\n if (['pie', 'doughnut', 'polarArea'].includes(type)) {\n return {\n backgroundColor: Object.values(dataset.data || {}).map((_, i) => this.getChartColor(i)),\n borderColor: this.getThemeColor('background'),\n borderWidth: 1,\n }\n }\n\n const color = this.getChartColor(index)\n return {\n backgroundColor: color,\n hoverBackgroundColor: color,\n borderColor: color,\n borderWidth: 1,\n }\n }\n\n // Function to set chart default colors\n setDefaultColorsForChart() {\n Chart.defaults.color = this.getThemeColor('muted-foreground') // font color\n Chart.defaults.borderColor = this.getThemeColor('border') // border color\n Chart.defaults.backgroundColor = this.getThemeColor('background') // background color\n\n // tooltip colors\n Chart.defaults.plugins.tooltip.backgroundColor = this.getThemeColor('background')\n Chart.defaults.plugins.tooltip.borderColor = this.getThemeColor('border')\n Chart.defaults.plugins.tooltip.titleColor = this.getThemeColor('foreground')\n Chart.defaults.plugins.tooltip.bodyColor = this.getThemeColor('muted-foreground')\n Chart.defaults.plugins.tooltip.borderWidth = 1\n\n // legend\n // options.plugins.legend.labels\n Chart.defaults.plugins.legend.labels.boxWidth = 12\n Chart.defaults.plugins.legend.labels.boxHeight = 12\n Chart.defaults.plugins.legend.labels.borderWidth = 0\n Chart.defaults.plugins.legend.labels.useBorderRadius = true\n Chart.defaults.plugins.legend.labels.borderRadius = 2 // px, matches shadcn's rounded-[2px] legend indicator\n }\n\n // Function to refresh the chart\n refreshChart() {\n // Destroy the chart if it's a valid Chart.js instance\n this.chart?.destroy()\n // Reinitialize the chart\n this.initChart()\n }\n\n // Function to initialize the dark mode observer\n initDarkModeObserver() {\n this.darkModeObserver = new MutationObserver(() => {\n this.refreshChart()\n })\n this.darkModeObserver.observe(document.documentElement, { attributeFilter: ['class'] })\n }\n\n // Function to merge the options with the defaults\n mergeOptionsWithDefaults() {\n return {\n ...this.optionsValue,\n data: {\n ...this.optionsValue.data,\n datasets: this.optionsValue.data.datasets.map((dataset, index) => {\n return {\n ...this.defaultThemeColor(dataset, index),\n ...dataset,\n }\n })\n }\n }\n }\n}\n" } ], "dependencies": { @@ -739,13 +739,18 @@ "gems": [] }, "install_command": "rails g ruby_ui:component Chart", - "docs_markdown": "# Chart\n\nDisplays information in a visual way.\n\n## Introduction\n\n## Usage\n\n### Bar Chart\n\n```ruby\noptions = {\n type: 'bar',\n data: {\n labels: ['Phlex', 'VC', 'ERB'],\n datasets: [{\n label: 'render time (ms)',\n data: [100, 520, 1200],\n }]\n },\n options: {\n indexAxis: 'y',\n scales: {\n y: {\n beginAtZero: true\n }\n },\n },\n}\n\nChart(options: options)\n```\n\n### Line Graph\n\n```ruby\noptions = {\n type: 'line',\n data: {\n labels: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'],\n datasets: [{\n label: 'Github Stars',\n data: [40, 30, 79, 140, 290, 550],\n }]\n },\n options: {\n scales: {\n y: {\n beginAtZero: true\n }\n },\n plugins: {\n legend: {\n display: false\n }\n }\n },\n}\n\nChart(options: options)\n```\n\n### Pie Chart\n\n```ruby\noptions = {\n type: 'pie',\n data: {\n labels: [\n 'Red',\n 'Blue',\n 'Yellow'\n ],\n datasets: [{\n label: 'My First Dataset',\n data: [300, 50, 100],\n backgroundColor: [\n 'rgb(255, 99, 132)',\n 'rgb(54, 162, 235)',\n 'rgb(255, 205, 86)'\n ],\n hoverOffset: 4\n }]\n },\n}\n\nChart(options: options)\n```", + "docs_markdown": "# Chart\n\nDisplays information in a visual way.\n\n## Introduction\n\n## Usage\n\n### Bar Chart\n\n```ruby\noptions = {\n type: 'bar',\n data: {\n labels: ['Phlex', 'VC', 'ERB'],\n datasets: [{\n label: 'render time (ms)',\n data: [100, 520, 1200],\n }]\n },\n options: {\n indexAxis: 'y',\n scales: {\n y: {\n beginAtZero: true\n }\n },\n },\n}\n\nChart(options: options)\n```\n\n### Multiple datasets\n\n```ruby\noptions = {\n type: 'bar',\n data: {\n labels: ['January', 'February', 'March', 'April', 'May', 'June'],\n datasets: [\n { label: 'Desktop', data: [186, 305, 237, 73, 209, 214] },\n { label: 'Mobile', data: [80, 200, 120, 190, 130, 140] }\n ]\n },\n}\n\nChart(options: options)\n```\n\n### Line Graph\n\n```ruby\noptions = {\n type: 'line',\n data: {\n labels: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'],\n datasets: [{\n label: 'Github Stars',\n data: [40, 30, 79, 140, 290, 550],\n }]\n },\n options: {\n scales: {\n y: {\n beginAtZero: true\n }\n },\n plugins: {\n legend: {\n display: false\n }\n }\n },\n}\n\nChart(options: options)\n```\n\n### Pie Chart\n\n```ruby\noptions = {\n type: 'pie',\n data: {\n labels: [\n 'Red',\n 'Blue',\n 'Yellow'\n ],\n datasets: [{\n label: 'My First Dataset',\n data: [300, 50, 100],\n backgroundColor: [\n 'rgb(255, 99, 132)',\n 'rgb(54, 162, 235)',\n 'rgb(255, 205, 86)'\n ],\n hoverOffset: 4\n }]\n },\n}\n\nChart(options: options)\n```", "examples": [ { "title": "Bar Chart", "code": "options = {\n type: 'bar',\n data: {\n labels: ['Phlex', 'VC', 'ERB'],\n datasets: [{\n label: 'render time (ms)',\n data: [100, 520, 1200],\n }]\n },\n options: {\n indexAxis: 'y',\n scales: {\n y: {\n beginAtZero: true\n }\n },\n },\n}\n\nChart(options: options)\n", "language": "ruby" }, + { + "title": "Multiple datasets", + "code": "options = {\n type: 'bar',\n data: {\n labels: ['January', 'February', 'March', 'April', 'May', 'June'],\n datasets: [\n { label: 'Desktop', data: [186, 305, 237, 73, 209, 214] },\n { label: 'Mobile', data: [80, 200, 120, 190, 130, 140] }\n ]\n },\n}\n\nChart(options: options)\n", + "language": "ruby" + }, { "title": "Line Graph", "code": "options = {\n type: 'line',\n data: {\n labels: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'],\n datasets: [{\n label: 'Github Stars',\n data: [40, 30, 79, 140, 290, 550],\n }]\n },\n options: {\n scales: {\n y: {\n beginAtZero: true\n }\n },\n plugins: {\n legend: {\n display: false\n }\n }\n },\n}\n\nChart(options: options)\n",