Skip to content

Commit

Permalink
Merge pull request #6 from alpaca-tc/render-only-modules
Browse files Browse the repository at this point in the history
Added option to show only modules to see dependencies between modules
  • Loading branch information
alpaca-tc authored Apr 10, 2024
2 parents 20f0577 + 229ff2b commit ee14086
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 55 deletions.
3 changes: 2 additions & 1 deletion frontend/pages/Home/Show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ export const Show: React.FC = () => {
const [graphOptions, setGraphOptions] = useLocalStorage<GraphOptions>('HomeShow-GraphOptions', {
compound: false,
concentrate: false,
onlyModule: false,
})
const {
data: combinedDefinition,
isLoading,
mutate: mutateCombinedDefinition,
} = useCombinedDefinition(selectedDefinitionIds, graphOptions.compound, graphOptions.concentrate)
} = useCombinedDefinition(selectedDefinitionIds, graphOptions.compound, graphOptions.concentrate, graphOptions.onlyModule)

const onCloseDialog = useCallback(() => {
setVisibleDialog(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { spacing } from '@/constants/theme'
export type GraphOptions = {
compound: boolean
concentrate: boolean
onlyModule: boolean
}

type Props = {
Expand Down Expand Up @@ -45,6 +46,13 @@ export const ConfigureGraphOptionsDialog: React.FC<Props> = ({ isOpen, onClickCl
[setTemporaryViewOptions],
)

const onChangeOnlyModule = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setTemporaryViewOptions((prev) => ({ ...prev, onlyModule: event.target.checked }))
},
[setTemporaryViewOptions],
)

return (
<ActionDialog
title="Configure Graph Options"
Expand Down Expand Up @@ -73,6 +81,13 @@ export const ConfigureGraphOptionsDialog: React.FC<Props> = ({ isOpen, onClickCl
>
<CheckBox name="compound" onChange={onChangeConcentrate} checked={temporaryViewOptions.concentrate} />
</FormControl>

<FormControl
title="Render only modules"
helpMessage="Displays only the dependencies between modules, not individual sources."
>
<CheckBox name="only_module" onChange={onChangeOnlyModule} checked={temporaryViewOptions.onlyModule} />
</FormControl>
</Stack>
</Stack>
</Stack>
Expand Down
3 changes: 2 additions & 1 deletion frontend/repositories/combinedDefinitionRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,11 @@ const fetchDefinitionShow = async (requestPath: string): Promise<CombinedDefinit

const toBooleanFlag = (value: boolean) => (value ? '1' : null)

export const useCombinedDefinition = (ids: number[], compound: boolean, concentrate: boolean) => {
export const useCombinedDefinition = (ids: number[], compound: boolean, concentrate: boolean, onlyModule: boolean) => {
const params = {
compound: toBooleanFlag(compound),
concentrate: toBooleanFlag(concentrate),
only_module: toBooleanFlag(onlyModule),
}
const requestPath = `${path.api.definitions.show(ids)}?${stringify(params)}`
const shouldFetch = ids.length > 0
Expand Down
3 changes: 2 additions & 1 deletion lib/diver_down/web.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def call(env)
bit_id = Regexp.last_match[:bit_id].to_i
compound = request.params['compound'] == '1'
concentrate = request.params['concentrate'] == '1'
action.combine_definitions(bit_id, compound, concentrate)
only_module = request.params['only_module'] == '1'
action.combine_definitions(bit_id, compound, concentrate, only_module)
in ['GET', %r{\A/api/sources/(?<source>[^/]+)\.json\z}]
source = Regexp.last_match[:source]
action.source(source)
Expand Down
5 changes: 3 additions & 2 deletions lib/diver_down/web/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ def pid
# @param bit_id [Integer]
# @param compound [Boolean]
# @param concentrate [Boolean]
def combine_definitions(bit_id, compound, concentrate)
# @param only_module [Boolean]
def combine_definitions(bit_id, compound, concentrate, only_module)
ids = DiverDown::Web::BitId.bit_id_to_ids(bit_id)

valid_ids = ids.select do
Expand All @@ -175,7 +176,7 @@ def combine_definitions(bit_id, compound, concentrate)
end

if definition
definition_to_dot = DiverDown::Web::DefinitionToDot.new(definition, @module_store, compound:, concentrate:)
definition_to_dot = DiverDown::Web::DefinitionToDot.new(definition, @module_store, compound:, concentrate:, only_module:)

json(
titles:,
Expand Down
165 changes: 134 additions & 31 deletions lib/diver_down/web/definition_to_dot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ module DiverDown
class Web
class DefinitionToDot
ATTRIBUTE_DELIMITER = ' '
MODULE_DELIMITER = '::'

# Between modules is prominently distanced
MODULE_MINLEN = 3

class MetadataStore
Metadata = Data.define(:id, :type, :data, :module_store) do
Expand Down Expand Up @@ -96,7 +100,13 @@ def issue_dependency_id(dependency)
# @param module_names [Array<String>]
# @return [String]
def issue_modules_id(module_names)
build_metadata_and_return_id(:module, module_names)
issued_metadata = @to_h.values.find { _1.type == :module && _1.data == module_names }

if issued_metadata
issued_metadata.id
else
build_metadata_and_return_id(:module, module_names)
end
end

# @param id [String]
Expand Down Expand Up @@ -132,14 +142,15 @@ def length
# @param module_store [DiverDown::ModuleStore]
# @param compound [Boolean]
# @param concentrate [Boolean] https://graphviz.org/docs/attrs/concentrate/
def initialize(definition, module_store, compound: false, concentrate: false)
def initialize(definition, module_store, compound: false, concentrate: false, only_module: false)
@definition = definition
@module_store = module_store
@io = DiverDown::IndentedStringIo.new
@indent = 0
@compound = compound
@compound = compound || only_module # When only-module is enabled, dependencies between modules are displayed as compound.
@compound_map = Hash.new { |h, k| h[k] = {} } # Hash{ ltail => Hash{ lhead => issued id } }
@concentrate = concentrate
@only_module = only_module
@metadata_store = MetadataStore.new(module_store)
end

Expand All @@ -150,14 +161,17 @@ def metadata

# @return [String]
def to_s
sources = definition.sources.sort_by(&:source_name)

io.puts %(strict digraph "#{definition.title}" {)
io.indented do
io.puts('compound=true') if @compound
io.puts('concentrate=true') if @concentrate
sources.each do
insert_source(_1)

if @only_module
render_only_modules
else
definition.sources.sort_by(&:source_name).each do
insert_source(_1)
end
end
end
io.puts '}'
Expand All @@ -168,6 +182,92 @@ def to_s

attr_reader :definition, :module_store, :io

def render_only_modules
# Hash{ from_module => { to_module => Array<DiverDown::Definition::Dependency> } }
dependency_map = Hash.new { |h, k| h[k] = Hash.new { |hi, ki| hi[ki] = [] } }

definition.sources.sort_by(&:source_name).each do |source|
source_modules = module_store.get(source.source_name)
next if source_modules.empty?

source.dependencies.each do |dependency|
dependency_modules = module_store.get(dependency.source_name)
next if dependency_modules.empty?

dependency_map[source_modules][dependency_modules].push(dependency)
end
end

# Remove duplicated prefix modules
# from [["A"], ["A", "B"]] to [["A", "B"]]
uniq_modules = [*dependency_map.keys, *dependency_map.values.map(&:keys).flatten(1)].uniq
uniq_modules.reject! do |modules|
modules.empty? ||
uniq_modules.any? { _1[0..modules.size - 1] == modules && _1.length > modules.size }
end

uniq_modules.each do |specific_module_names|
buf = swap_io do
indexes = (0..(specific_module_names.length - 1)).to_a

chain_yield(indexes) do |index, next_proc|
module_names = specific_module_names[0..index]
module_name = specific_module_names[index]

io.puts %(subgraph "#{module_label(module_names)}" {)
io.indented do
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
io.puts %(label="#{module_name}")
io.puts %("#{module_name}" #{build_attributes(label: module_name, id: @metadata_store.issue_modules_id(module_names))})

next_proc&.call
end
io.puts '}'
end
end

io.write buf.string
end

dependency_map.each do |from_modules, h|
h.each do |to_modules, all_dependencies|
# Do not render standalone source
# Do not render self-dependency
next if from_modules.empty? || to_modules.empty? || from_modules == to_modules

dependencies = DiverDown::Definition::Dependency.combine(*all_dependencies)

dependencies.each do
attributes = {}
ltail = module_label(*from_modules)
lhead = module_label(*to_modules)

# Already rendered dependencies between modules
# Add the dependency to the edge of the compound
if @compound_map[ltail].include?(lhead)
compound_id = @compound_map[ltail][lhead]
@metadata_store.append_dependency(compound_id, _1)
next
end

compound_id = @metadata_store.issue_dependency_id(_1)
@compound_map[ltail][lhead] = compound_id

attributes.merge!(
id: compound_id,
ltail:,
lhead:,
minlen: MODULE_MINLEN
)

io.write(%("#{from_modules[-1]}" -> "#{to_modules[-1]}"))
io.write(%( #{build_attributes(**attributes)}), indent: false) unless attributes.empty?
io.write("\n")
end
end
end
end

def insert_source(source)
if module_store.get(source.source_name).empty?
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
Expand Down Expand Up @@ -199,7 +299,7 @@ def insert_source(source)
id: compound_id,
ltail:,
lhead:,
minlen: (between_modules ? 3 : nil) # Between modules is prominently distanced
minlen: MODULE_MINLEN
)
else
attributes.merge!(
Expand All @@ -216,42 +316,45 @@ def insert_source(source)
def insert_modules(source)
buf = swap_io do
all_module_names = module_store.get(source.source_name)
*head_module_indexes, _tail_module_index = (0..(all_module_names.length - 1)).to_a
indexes = (0..(all_module_names.length - 1)).to_a

# last subgraph
last_module_writer = proc do
module_names = all_module_names
chain_yield(indexes) do |index, next_proc|
module_names = all_module_names[0..index]
module_name = module_names[-1]

io.puts %(subgraph "#{module_label(module_name)}" {)
io.puts %(subgraph "#{module_label(module_names)}" {)
io.indented do
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
io.puts %(label="#{module_name}")
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})

if next_proc
next_proc.call
else
# last. equals indexes[-1] == index
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_source_id(source))})
end
end
io.puts '}'
end
end

# wrapper subgraph
modules_writer = head_module_indexes.inject(last_module_writer) do |next_writer, module_index|
proc do
module_names = all_module_names[0..module_index]
module_name = module_names[-1]
io.write buf.string
end

io.puts %(subgraph "#{module_label(module_name)}" {)
io.indented do
io.puts %(id="#{@metadata_store.issue_modules_id(module_names)}")
io.puts %(label="#{module_name}")
next_writer.call
end
io.puts '}'
end
end
def chain_yield(values, &block)
*head, tail = values

modules_writer.call
last_proc = proc do
block.call(tail, nil)
end

io.write buf.string
chain_proc = head.inject(last_proc) do |next_proc, value|
proc do
block.call(value, next_proc)
end
end

chain_proc.call
end

# rubocop:disable Lint/UnderscorePrefixedVariableName
Expand Down Expand Up @@ -289,7 +392,7 @@ def swap_io
def module_label(*modules)
return if modules.empty?

"cluster_#{modules[0]}"
"cluster_#{modules.join(MODULE_DELIMITER)}"
end
end
end
Expand Down
Loading

0 comments on commit ee14086

Please sign in to comment.