Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/app/views/docs/chart.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand Down
36 changes: 26 additions & 10 deletions gem/lib/ruby_ui/chart/chart_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand All @@ -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
Expand All @@ -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,
}
})
Expand Down
25 changes: 25 additions & 0 deletions gem/lib/ruby_ui/chart/chart_docs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand Down
9 changes: 7 additions & 2 deletions mcp/data/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
"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",
Expand Down
Loading