diff --git a/doc/source/_static/css/examples.css b/doc/source/_static/css/examples.css index bb641d75147f..cff0aa9ddd58 100644 --- a/doc/source/_static/css/examples.css +++ b/doc/source/_static/css/examples.css @@ -1,89 +1,108 @@ :root { --ray-example-gallery-gap-x: 18px; --ray-example-gallery-gap-y: 22px; - --sidebar-top: 5em; } -#site-navigation { - width: 330px !important; - border-right: none; - margin-left: 32px; - overflow-y: auto; - max-height: calc(100vh - var(--sidebar-top)); - position: sticky; - top: var(--sidebar-top) !important; - z-index: 1000; +.gallery-sidebar { + width: 100%; } -#site-navigation h5 { +.gallery-sidebar h5 { font-size: 16px; font-weight: 600; - color: #000; } -#site-navigation h6 { +.gallery-sidebar h6 { font-size: 14px; font-weight: 600; - color: #000; text-transform: uppercase; } -/* Hide the default sidebar content */ -#site-navigation > div.bd-sidebar__content { - display: none; +.searchDiv { + margin-bottom: 2em; + display: flex; + flex-direction: row; + border: 1px solid var(--pst-color-border); + border-radius: 4px; + background-color: var(--pst-color-on-background); + justify-content: center; + align-items: center; + height: 50px; } -#site-navigation > div.rtd-footer-container { - display: none; + +#search-input-label { + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; } -.searchDiv { - margin-bottom: 2em; +.searchDiv:focus-within { + outline: 2px solid var(--pst-color-border); +} + +#search-icon { + fill: var(--pst-color-text-base); + margin: 0em 1em; } #searchInput { width: 100%; - color: #5F6469; - border: 1px solid #D2DCE6; - height: 50px; - border-radius: 4px; - background-color: #F9FAFB; - background-image: url("data:image/svg+xml,%3Csvg width='25' height='25' viewBox='0 0 25 25' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Systems / search-line' clip-path='url(%23clip0_1_150)'%3E%3Crect width='24' height='24' transform='translate(0.398529 0.0546875)' fill='%23F9FAFB'/%3E%3Cg id='Group'%3E%3Cpath id='Vector' d='M18.4295 16.6717L22.7125 20.9537L21.2975 22.3687L17.0155 18.0857C15.4223 19.3629 13.4405 20.0576 11.3985 20.0547C6.43053 20.0547 2.39853 16.0227 2.39853 11.0547C2.39853 6.08669 6.43053 2.05469 11.3985 2.05469C16.3665 2.05469 20.3985 6.08669 20.3985 11.0547C20.4014 13.0967 19.7068 15.0784 18.4295 16.6717ZM16.4235 15.9297C17.6926 14.6246 18.4014 12.8751 18.3985 11.0547C18.3985 7.18669 15.2655 4.05469 11.3985 4.05469C7.53053 4.05469 4.39853 7.18669 4.39853 11.0547C4.39853 14.9217 7.53053 18.0547 11.3985 18.0547C13.219 18.0576 14.9684 17.3488 16.2735 16.0797L16.4235 15.9297V15.9297Z' fill='%238C9196'/%3E%3C/g%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_1_150'%3E%3Crect width='24' height='24' fill='white' transform='translate(0.398529 0.0546875)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A"); - background-repeat: no-repeat; - background-position-x: 0.5em; - background-position-y: center; - background-size: 1.5em; - padding-left: 3em; + color: var(--pst-color-text-base); + border: none; + background-color: transparent; +} + +#searchInput:focus-visible { + outline: none; } #searchInput::placeholder { - color: #5F6469; + color: var(--pst-color-text-muted); opacity: 1; } -.tag { - margin-bottom: 5px; - font-size: small; - color: #000000; - border: 1px solid #D2DCE6; - border-radius: 14px; - display: flex; - flex-direction: row; - align-items: center; - width: fit-content; - gap: 1em; +/* Tag button styling */ +.tag-group { + display: flex; + flex-flow: wrap; + gap: 0em 0.5em; } - -.tag.btn-outline-primary { - color: #000000; +.tag { + margin-bottom: 5px; + font-size: small; + background-color: var(--pst-color-on-background); + border: 1px solid var(--pst-color-border); + border-radius: 14px; + display: flex; + flex-direction: row; + align-items: center; + width: fit-content; + gap: 0.5em; + color: var(--pst-color-text-base); padding: 3px 12px 3px 12px; line-height: 20px; } -.tag-btn-wrapper { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 1em; +.tag > svg > path { + fill: var(--pst-color-text-base); +} +.tag:hover { + background-color: var(--pst-color-info-highlight) !important; + color: var(--pst-color-info-text) !important; +} +.tag:hover > svg > path { + fill: var(--pst-color-info-text) !important; +} + +/* Selected tag buttons */ +.tag.btn-primary { + background-color: var(--pst-color-info-bg); +} +/* Inactive tag buttons */ +.tag.btn-outline-primary { + background-color: var(--pst-color-on-background); } div.sd-container-fluid.docutils > div { @@ -103,9 +122,10 @@ div.gallery-item { width: auto; } +/* Override sphinx-design box shadow styles */ div.gallery-item > div.sd-card { - border-radius: 8px; - box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.05) !important; + box-shadow: 0 .125rem .25rem var(--pst-color-shadow) !important; + border: none; } /* Example gallery "Tutorial" title */ @@ -148,18 +168,6 @@ div.sd-card-title > span.sd-bg-info.sd-bg-text-info { background-color: initial !important; } -div.sd-card-body > p.sd-card-text > a { - text-align: initial; -} - -div.sd-card-body > p.sd-card-text > a > span { - color: rgb(81, 81, 81); -} - -#main-content { - max-width: 100%; -} - #noMatches { display: flex; flex-direction: column; @@ -177,47 +185,3 @@ div.sd-card-body > p.sd-card-text > a > span { #noMatches.hidden,.gallery-item.hidden { display: none !important; } - -.btn-primary { - color: #004293; - background: rgba(61, 138, 233, 0.20); - padding: 3px 12px 3px 12px; - border: 1px solid #D2DCE6; -} - -button.try-anyscale { - background-color: initial !important; - width: fit-content; - padding: 0 !important; - margin-left: auto !important; - float: initial !important; -} - -button.try-anyscale > svg { - display: none; -} - -button.try-anyscale > i { - display: none; -} - -button.try-anyscale > span { - margin: 0; - text-decoration-line: underline; - font-weight: 500; - color: #000; -} - -.top-nav-content { - justify-content: initial; -} - -/* Hide nav bar that has github, fullscreen, and print icons */ -div.header-article.row.sticky-top.noprint { - display: none !important; -} - -/* Hide the footer with 'prev article' and 'next article' buttons */ -.footer-article.hidden { - display: none !important; -} diff --git a/doc/source/_static/js/examples.js b/doc/source/_static/js/examples.js new file mode 100644 index 000000000000..b31e36f9abfa --- /dev/null +++ b/doc/source/_static/js/examples.js @@ -0,0 +1,111 @@ +/** + * Check whether a panel matches the selected filter tags. + * + * @param {any} panel Example gallery item + * @param {Array>} groupedActiveTags Groups of tags selected by the user. + * @returns {boolean} True if the panel should be shown, false otherwise + */ +function panelMatchesTags(panel, groupedActiveTags) { + // Show the panel if every tagGroup has at least one active tag in the classList, + // or if no tag in a group is selected. + return groupedActiveTags.every(tagGroup => { + return tagGroup.length === 0 || Array.from(panel.classList).some(tag => tagGroup.includes(tag)) + }) +} + + +window.addEventListener('load', () => { + + /* Fetch the tags that the user can filter on from the buttons in the sidebar + * Additionally retrieve the elements that we need for filtering. + */ + const tags = {} + document.querySelectorAll('div.tag-section').forEach(group => { + tags[group.id] = group.querySelectorAll('div.tag-group > div.tag.btn') + }) + const noMatchesElement = document.querySelector("#noMatches"); + const panels = document.querySelectorAll('.gallery-item') + + /** + * Filter the links to the examples in the example gallery + * by the selected tags and the current search query. + */ + function filterPanels() { + const query = document.getElementById("searchInput").value.toLowerCase(); + const activeTags = Array.from(document.querySelectorAll('.tag.btn-primary')).map(el => el.id); + const groupedActiveTags = Object.values(tags).map(group => { + const tagNames = Array.from(group).map(element => element.id); + return activeTags.filter(activeTag => tagNames.includes(activeTag)); + }) + + // Show all panels first + panels.forEach(panel => panel.classList.remove("hidden")); + + let toHide = []; + let toShow = []; + + // Show each panel if it has every active tag and matches the search query + panels.forEach(panel => { + const text = (panel.textContent + panel.classList.toString()).toLowerCase(); + // const hasTag = activeTags.every(tag => panel.classList.contains(tag)); + const hasTag = panelMatchesTags(panel, groupedActiveTags) + const hasText = text.includes(query.toLowerCase()); + + if (hasTag && hasText) { + toShow.push(panel); + } else { + toHide.push(panel); + } + }) + + toShow.forEach(panel => panel.classList.remove("hidden")); + toHide.forEach(panel => panel.classList.add("hidden")); + + // If no matches are found, display the noMatches element + if (toShow.length === 0) { + noMatchesElement.classList.remove("hidden"); + } else { + noMatchesElement.classList.add("hidden"); + } + + // Set the URL to match the active tags using query parameters + history.replaceState(null, null, activeTags.length === 0 ? location.pathname : `?tags=${activeTags.join(',')}`); + } + + // Generate the callback triggered when a user clicks on a tag filter button. + document.querySelectorAll('.tag').forEach(tag => { + tag.addEventListener('click', () => { + // Toggle "tag" buttons on click. + if (tag.classList.contains('btn-primary')) { + // deactivate filter button + tag.classList.replace('btn-primary', 'btn-outline-primary'); + } else { + // activate filter button + tag.classList.replace('btn-outline-primary', 'btn-primary'); + } + filterPanels() + }); + }); + + // Add event listener for keypresses in the search bar + const searchInput = document.getElementById("searchInput"); + if (searchInput) { + searchInput.addEventListener("keyup", function (event) { + event.preventDefault(); + filterPanels(); + }); + } + + // Add the ability to provide URL query parameters to filter examples on page load + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.size > 0) { + const urlTagParams = urlParams.get('tags').split(','); + urlTagParams.forEach(tag => { + const tagButton = document.getElementById(tag); + if (tagButton) { + tagButton.classList.replace('btn-outline-primary', 'btn-primary'); + } + }); + filterPanels(); + } +}); diff --git a/doc/source/_static/js/tags.js b/doc/source/_static/js/tags.js deleted file mode 100644 index 99ac9db7d5ca..000000000000 --- a/doc/source/_static/js/tags.js +++ /dev/null @@ -1,400 +0,0 @@ -/** - * Generate a button that will be used for filtering example gallery items. - * - * @param {string} id ID of the element to use - * @param {string} text Text do display to the user inside the button - * @param {string} icon Icon to show next to the text. - * @returns {string} Inner HTML that contains the tag filter button. - */ -function generateTagButton(id, text, icon) { - return ` -
${icon}${text}
- `; -} - -/** - * Generate the inner HTML for the tag filter buttons in the sidebar. - * - * @param {string} name Name of the section - * @param {Array} tags Array of tags HTML generated from generateTagButton - * @returns {string} Inner HTML to display the sidebar - */ -function generateTagSection(name, tags) { - return ` -
${name}
-
${tags.join('')}
- ` -} - -/** - * Check whether a panel matches the selected filter tags. - * - * @param {any} panel Example gallery item - * @param {Array>} groupedActiveTags Groups of tags selected by the user. - * @returns {boolean} True if the panel should be shown, false otherwise - */ -function panelMatchesTags(panel, groupedActiveTags) { - // Show the panel if every tagGroup has at least one active tag in the classList, - // or if no tag in a group is selected. - return groupedActiveTags.every(tagGroup => { - return tagGroup.length === 0 || Array.from(panel.classList).some(tag => tagGroup.includes(tag)) - }) -} - - -/** - * Filter the links to the examples in the example gallery - * by the selected tags and the current search query. - * - * @param {object} tags Object with grouped arrays of tags, each with className, displayName, and - * icon - */ -function filterPanels(tags) { - const noMatchesElement = document.querySelector("#noMatches"); - const query = document.getElementById("searchInput").value.toLowerCase(); - const panels = document.querySelectorAll('.gallery-item') - const activeTags = Array.from(document.querySelectorAll('.tag.btn-primary')).map(el => el.id); - const groupedActiveTags = Object.values(tags).map(tagGroup => { - const classes = tagGroup.map(({className}) => className); - return activeTags.filter(activeTag => classes.includes(activeTag)); - }) - - // Show all panels first - panels.forEach(panel => panel.classList.remove("hidden")); - - let toHide = []; - let toShow = []; - - // Show each panel if it has every active tag and matches the search query - panels.forEach(panel => { - const text = (panel.textContent + panel.classList.toString()).toLowerCase(); - // const hasTag = activeTags.every(tag => panel.classList.contains(tag)); - const hasTag = panelMatchesTags(panel, groupedActiveTags) - const hasText = text.includes(query.toLowerCase()); - - if (hasTag && hasText) { - toShow.push(panel); - } else { - toHide.push(panel); - } - }) - - toShow.forEach(panel => panel.classList.remove("hidden")); - toHide.forEach(panel => panel.classList.add("hidden")); - - // If no matches are found, display the noMatches element - if (toShow.length === 0) { - noMatchesElement.classList.remove("hidden"); - } else { - noMatchesElement.classList.add("hidden"); - } - - // Set the URL to match the active tags using query parameters - history.replaceState(null, null, activeTags.length === 0 ? '' : `?tags=${activeTags.join(',')}`); -} - -/** - * Generate the callback triggered when a user clicks on a tag filter button. - * @param {string} tag The element corresponding to the tag - * @returns {() => void} The callback that will be called when the user clicks a tag filter button - */ -function generateTagClickHandler(tag, tags) { - return () => { - // Toggle "tag" buttons on click. - if (tag.classList.contains('btn-primary')) { - // deactivate filter button - tag.classList.replace('btn-primary', 'btn-outline-primary'); - } else { - // activate filter button - tag.classList.replace('btn-outline-primary', 'btn-primary'); - } - filterPanels(tags) - } -} - -window.addEventListener('load', () => { - // Sidebar icons - const rlIcon = ` - - - - `; - const otherIcon = ` - - - - `; - const llmIcon = ` - - - - `; - const genAiIcon = ` - - - - `; - const nlpIcon = ` - - - - `; - const timeSeriesIcon = ` - - - - `; - const computerVisionIcon = ` - - - - ` - const batchInferenceIcon = ` - - - - ` - const dataPreprocessingIcon = ` - - - - ` - const dataValidationIcon = ` - - - - ` - const experimentTrackingIcon = ` - - - - ` - const hyperparameterTuningIcon = ` - - - - ` - const modelTrainingIcon = ` - - - - ` - const monitoringIcon = ` - - - - ` - const orchestrationIcon = ` - - - - ` - const servingIcon = ` - - - - ` - const tensorflowIcon = ` - - - - - - image/svg+xml - - - - - - - - - - - - - - ` - const pytorchIcon = ` - - - - - - - ` - const kerasIcon = ` - - - - - - - - - - - - - ` - const huggingfaceIcon = ` - - - - - - - - - ` - - const isGallery = window.location.pathname.includes("ray-overview/examples.html") - if (isGallery) { - const tags = { - useCaseTags: [ - { - className: 'llm', - displayName: 'Large Language Models', - icon: llmIcon - }, - { - className: 'gen-ai', - displayName: 'Generative AI', - icon: genAiIcon - }, - { - className: 'cv', - displayName: 'Computer Vision', - icon: computerVisionIcon, - }, - { - className: 'ts', - displayName: 'Time-series', - icon: timeSeriesIcon, - }, - { - className: 'nlp', - displayName: 'Natural Language Processing', - icon: nlpIcon, - }, - { - className: 'rl', - displayName: 'Reinforcement Learning', - icon: rlIcon, - }, - ], - workloadTags: [ - { - className: 'data-processing', - displayName: 'Data Processing', - icon: dataPreprocessingIcon, - }, - { - className: 'training', - displayName: 'Model Training & Fine-tuning', - icon: modelTrainingIcon, - }, - { - className: 'tuning', - displayName: 'Hyperparameter Tuning', - icon: hyperparameterTuningIcon, - }, - { - className: 'inference', - displayName: 'Batch Inference', - icon: batchInferenceIcon, - }, - { - className: 'serving', - displayName: 'Model Serving', - icon: servingIcon, - }, - ], - mlopsTags: [ - { - className: 'tracking', - displayName: 'Experiment Tracking', - icon: experimentTrackingIcon, - }, - { - className: 'monitoring', - displayName: 'Monitoring', - icon: monitoringIcon, - }, - ], - frameworkTags: [ - { - className: 'pytorch', - displayName: 'PyTorch', - icon: pytorchIcon, - }, - { - className: 'huggingface', - displayName: 'HuggingFace', - icon: huggingfaceIcon, - }, - { - className: 'tensorflow', - displayName: 'Tensorflow/Keras', - icon: tensorflowIcon, - }, - ] - } - - const useCaseTags = tags.useCaseTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - const workloadTags = tags.workloadTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - const mlopsTags = tags.mlopsTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - const frameworkTags = tags.frameworkTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - - - const tagString = "
\n" + - "
Filter by

\n" + - generateTagSection("USE CASES", useCaseTags) + - generateTagSection("ML WORKLOADS", workloadTags) + - generateTagSection("MLOPS", mlopsTags) + - generateTagSection("FRAMEWORKS", frameworkTags) + - "
"; - - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = tagString; - const newNav = tempDiv.firstChild; - - // Populate the sidebar with buttons that filter for different example tags - document.getElementById("site-navigation").appendChild(newNav); - - document.querySelectorAll('.tag').forEach(tag => { - tag.addEventListener('click', generateTagClickHandler(tag, tags)); - }); - - const searchInput = document.getElementById("searchInput"); - if (searchInput) { - searchInput.addEventListener("keyup", function (event) { - event.preventDefault(); - filterPanels(tags); - }); - } - - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.size > 0) { - const urlTagParams = urlParams.get('tags').split(','); - urlTagParams.forEach(tag => { - const tagButton = document.getElementById(tag); - if (tagButton) { - tagButton.classList.replace('btn-outline-primary', 'btn-primary'); - } - }); - filterPanels(tags); - } - } -}); diff --git a/doc/source/_templates/examples-sidebar.html b/doc/source/_templates/examples-sidebar.html new file mode 100644 index 000000000000..c16d49676b42 --- /dev/null +++ b/doc/source/_templates/examples-sidebar.html @@ -0,0 +1,139 @@ + diff --git a/doc/source/ray-overview/examples.rst b/doc/source/ray-overview/examples.rst index c1de39acb5ef..cd36e8158193 100644 --- a/doc/source/ray-overview/examples.rst +++ b/doc/source/ray-overview/examples.rst @@ -5,21 +5,45 @@ Ray Examples .. raw:: html - -
- + +